diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..fe1edf2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,32 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: pomponchik
+
+---
+
+## Short description
+
+Replace this text with a short description of the error and the behavior that you expected to see instead.
+
+
+## Describe the bug in detail
+
+Please add this test in such a way that it reproduces the bug you found and does not pass:
+
+```python
+def test_your_bug():
+ ...
+```
+
+Writing the test, please keep compatibility with the [`pytest`](https://docs.pytest.org/) framework.
+
+If for some reason you cannot describe the error in the test format, describe here the steps to reproduce it.
+
+
+## Environment
+ - OS: ...
+ - Python version (the output of the `python --version` command): ...
+ - Version of this package: ...
diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md
new file mode 100644
index 0000000..20f4742
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/documentation.md
@@ -0,0 +1,26 @@
+---
+name: Documentation fix
+about: Add something to the documentation, delete it, or change it
+title: ''
+labels: documentation
+assignees: pomponchik
+---
+
+## It's cool that you're here!
+
+Documentation is an important part of the project, we strive to make it high-quality and keep it up to date. Please adjust this template by outlining your proposal.
+
+
+## Type of action
+
+What do you want to do: remove something, add it, or change it?
+
+
+## Where?
+
+Specify which part of the documentation you want to make a change to? For example, the name of an existing documentation section or the line number in a file `README.md`.
+
+
+## The essence
+
+Please describe the essence of the proposed change
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..3d12c06
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,17 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: enhancement
+assignees: pomponchik
+
+---
+
+## Short description
+
+What do you propose and why do you consider it important?
+
+
+## Some details
+
+If you can, provide code examples that will show how your proposal will work. Also, if you can, indicate which alternatives to this behavior you have considered. And finally, how do you propose to test the correctness of the implementation of your idea, if at all possible?
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
new file mode 100644
index 0000000..40fe808
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -0,0 +1,12 @@
+---
+name: Question or consultation
+about: Ask anything about this project
+title: ''
+labels: question
+assignees: pomponchik
+
+---
+
+## Your question
+
+Here you can freely describe your question about the project. Please, before doing this, read the documentation provided, and ask the question only if the necessary answer is not there. In addition, please keep in mind that this is a free non-commercial project and user support is optional for its author. The response time is not guaranteed in any way.
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..07dd881
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,50 @@
+name: Lint
+
+on: push
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"]
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Cache pip dependencies
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ github.workflow }}-${{ hashFiles('requirements_dev.txt') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-${{ github.workflow }}-
+
+ - name: Install dependencies
+ shell: bash
+ run: pip install -r requirements_dev.txt
+
+ - name: Install the library
+ shell: bash
+ run: pip install .
+
+ - name: Run ruff
+ shell: bash
+ run: ruff check getsources
+
+ - name: Run ruff for tests
+ shell: bash
+ run: ruff check tests
+
+ - name: Run mypy
+ shell: bash
+ run: mypy --strict getsources
+
+ - name: Run mypy for tests
+ shell: bash
+ run: mypy tests
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..8b3e1df
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,34 @@
+name: Release
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ pypi-publish:
+ name: upload release to PyPI
+ runs-on: ubuntu-latest
+ # Specifying a GitHub environment is optional, but strongly encouraged
+ environment: release
+ permissions:
+ # IMPORTANT: this permission is mandatory for trusted publishing
+ id-token: write
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install dependencies
+ shell: bash
+ run: pip install -r requirements_dev.txt
+
+ - name: Build the project
+ shell: bash
+ run: python -m build .
+
+ - name: Publish package distributions to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/.github/workflows/tests_and_coverage.yml b/.github/workflows/tests_and_coverage.yml
new file mode 100644
index 0000000..46737ad
--- /dev/null
+++ b/.github/workflows/tests_and_coverage.yml
@@ -0,0 +1,56 @@
+name: Tests
+
+on: push
+
+jobs:
+ build:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [macos-latest, ubuntu-latest, windows-latest]
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"]
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install the library
+ shell: bash
+ run: pip install .
+
+ - name: Cache pip dependencies
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ github.workflow }}-${{ hashFiles('requirements_dev.txt') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-${{ github.workflow }}-
+
+ - name: Install dependencies
+ shell: bash
+ run: pip install -r requirements_dev.txt
+
+ - name: Print all libs
+ shell: bash
+ run: pip list
+
+ - name: Run tests and show coverage on the command line
+ run: |
+ coverage run --source=getsources --omit="*tests*" -m pytest --cache-clear --assert=plain && coverage report -m --fail-under=100
+ coverage xml
+
+ - name: Upload coverage to Coveralls
+ if: runner.os == 'Linux'
+ env:
+ COVERALLS_REPO_TOKEN: ${{secrets.COVERALLS_REPO_TOKEN}}
+ uses: coverallsapp/github-action@v2
+ with:
+ format: cobertura
+ file: coverage.xml
+ continue-on-error: true
+
+ - name: Run tests and show the branch coverage on the command line
+ run: coverage run --branch --source=getsources --omit="*tests*" -m pytest --cache-clear --assert=plain && coverage report -m --fail-under=100
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d333cb0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+venv
+.pytest_cache
+*.egg-info
+build
+dist
+__pycache__
+.idea
+test.py
+.mypy_cache
+.ruff_cache
+.DS_Store
+.ipynb_checkpoints
+.coverage
+notes.txt
diff --git a/README.md b/README.md
index 1d66731..a51b5f8 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,70 @@
-# getsource
-A way to get the source code of functions
+
+ ⓘ
+
+[](https://pepy.tech/project/getsources)
+[](https://pepy.tech/project/getsources)
+[](https://coveralls.io/github/mutating/getsources?branch=main)
+[](https://github.com/boyter/scc/)
+[](https://hitsofcode.com/github/mutating/getsources/view?branch=main)
+[](https://github.com/mutating/getsources/actions/workflows/tests_and_coverage.yml)
+[](https://pypi.python.org/pypi/getsources)
+[](https://badge.fury.io/py/getsources)
+[](http://mypy-lang.org/)
+[](https://github.com/astral-sh/ruff)
+[](https://deepwiki.com/mutating/getsources)
+
+
+
+
+
+
+This library is needed to obtain the source code of functions at runtime. It can be used, for example, as a basis for libraries that work with [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree) on the fly. In fact, it is a thin layer built around [`inspect.getsource`](https://docs.python.org/3/library/inspect.html#inspect.getsource) and [`dill.source.getsource`](https://dill.readthedocs.io/en/latest/dill.html#dill.source.getsource).
+
+
+## Installation
+
+You can install [`getsources`](https://pypi.python.org/pypi/getsources) using pip:
+
+```bash
+pip install getsources
+```
+
+You can also quickly try out this and other packages without having to install using [instld](https://github.com/pomponchik/instld).
+
+
+## Usage
+
+The basic function of the library is `getsource`, which works similarly to the function of the same name from the standard library:
+
+```python
+from getsources import getsource
+
+def function():
+ ...
+
+print(getsource(function))
+#> def function():
+#> ...
+```
+
+Unlike its counterpart from the standard library, this thing can also work:
+
+- With lambda functions
+- With functions defined inside REPL
+
+We also often need to trim excess indentation from a function object to make it easier to further process the resulting code. To do this, use the `getclearsource` function:
+
+```python
+from getsources import getclearsource
+
+class SomeClass:
+ @staticmethod
+ def method():
+ ...
+
+print(getclearsource(SomeClass.method))
+#> def method():
+#> ...
+```
+
+As you can see, the resulting source code text has no extra indentation, but in all other respects this function is completely identical to the usual `getsource`.
diff --git a/docs/assets/logo_1.svg b/docs/assets/logo_1.svg
new file mode 100644
index 0000000..f841438
--- /dev/null
+++ b/docs/assets/logo_1.svg
@@ -0,0 +1,80 @@
+
+
diff --git a/getsources/__init__.py b/getsources/__init__.py
new file mode 100644
index 0000000..ca36d06
--- /dev/null
+++ b/getsources/__init__.py
@@ -0,0 +1,2 @@
+from getsources.base import getsource as getsource
+from getsources.clear import getclearsource as getclearsource
diff --git a/getsources/base.py b/getsources/base.py
new file mode 100644
index 0000000..0c99557
--- /dev/null
+++ b/getsources/base.py
@@ -0,0 +1,11 @@
+from inspect import getsource as original_getsource
+from typing import Any, Callable
+
+from dill.source import getsource as dill_getsource # type: ignore[import-untyped]
+
+
+def getsource(function: Callable[..., Any]) -> str:
+ try:
+ return original_getsource(function)
+ except OSError: # pragma: no cover
+ return dill_getsource(function) # type: ignore[no-any-return]
diff --git a/getsources/clear.py b/getsources/clear.py
new file mode 100644
index 0000000..4911f82
--- /dev/null
+++ b/getsources/clear.py
@@ -0,0 +1,20 @@
+from typing import Any, Callable
+
+from getsources import getsource
+
+
+def getclearsource(function: Callable[..., Any]) -> str:
+ source_code = getsource(function)
+
+ splitted_source_code = source_code.split('\n')
+
+ indent = 0
+ for letter in splitted_source_code[0]: # pragma: no branch
+ if letter.isspace():
+ indent += 1
+ else:
+ break
+
+ new_splitted_source_code = [x[indent:] for x in splitted_source_code]
+
+ return '\n'.join(new_splitted_source_code).rstrip('\n')
diff --git a/getsources/py.typed b/getsources/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..5d8d5de
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,52 @@
+[build-system]
+requires = ["setuptools==68.0.0"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "getsources"
+version = "0.0.1"
+authors = [
+ { name="Evgeniy Blinov", email="zheni-b@yandex.ru" },
+]
+description = 'A way to get the source code of functions'
+readme = "README.md"
+requires-python = ">=3.8"
+dependencies = [
+ 'dill==0.4.0',
+]
+classifiers = [
+ "Operating System :: OS Independent",
+ 'Operating System :: MacOS :: MacOS X',
+ 'Operating System :: Microsoft :: Windows',
+ 'Operating System :: POSIX',
+ 'Operating System :: POSIX :: Linux',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 3.8',
+ 'Programming Language :: Python :: 3.9',
+ 'Programming Language :: Python :: 3.10',
+ 'Programming Language :: Python :: 3.11',
+ 'Programming Language :: Python :: 3.12',
+ 'Programming Language :: Python :: 3.13',
+ 'Programming Language :: Python :: 3.14',
+ 'Programming Language :: Python :: Free Threading',
+ 'Programming Language :: Python :: Free Threading :: 3 - Stable',
+ 'License :: OSI Approved :: MIT License',
+ 'Intended Audience :: Developers',
+ 'Topic :: Software Development :: Libraries',
+]
+keywords = ['inspect']
+
+[tool.setuptools.package-data]
+"getsources" = ["py.typed"]
+
+[tool.pytest.ini_options]
+markers = ["mypy_testing"]
+
+[tool.ruff]
+lint.ignore = ['E501', 'E712', 'PTH123', 'PTH118', 'PLR2004', 'PTH107', 'SIM105', 'SIM102', 'RET503', 'PLR0912', 'C901', 'E731']
+lint.select = ["ERA001", "YTT", "ASYNC", "BLE", "B", "A", "COM", "INP", "PIE", "T20", "PT", "RSE", "RET", "SIM", "SLOT", "TID252", "ARG", "PTH", "I", "C90", "N", "E", "W", "D201", "D202", "D419", "F", "PL", "PLE", "PLR", "PLW", "RUF", "TRY201", "TRY400", "TRY401"]
+format.quote-style = "single"
+
+[project.urls]
+'Source' = 'https://github.com/mutating/getsources'
+'Tracker' = 'https://github.com/mutating/getsources/issues'
diff --git a/requirements_dev.txt b/requirements_dev.txt
new file mode 100644
index 0000000..0da7190
--- /dev/null
+++ b/requirements_dev.txt
@@ -0,0 +1,11 @@
+pytest==8.0.2
+coverage==7.6.1
+build==1.2.2.post1
+twine==6.1.0
+mypy==1.14.1
+ruff==0.14.6
+pytest-mypy-testing==0.1.3
+full_match==0.0.3
+transfunctions==0.0.9
+pexpect==4.9.0; sys_platform == "linux" or sys_platform == "darwin"
+setuptools==75.3.4
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_base.py b/tests/test_base.py
new file mode 100644
index 0000000..cc48b3b
--- /dev/null
+++ b/tests/test_base.py
@@ -0,0 +1,136 @@
+import re
+from io import StringIO
+from os import environ
+from sys import platform
+
+import pytest
+
+from getsources import getsource
+
+
+def test_usual_functions():
+ def function_1():
+ pass
+
+ def function_2(a, b):
+ pass
+
+ assert getsource(function_1).splitlines() == [' def function_1():', ' pass']
+ assert getsource(function_2).splitlines() == [' def function_2(a, b):', ' pass']
+
+
+def test_lambda():
+ function = lambda x: x
+
+ assert getsource(function).strip() == 'function = lambda x: x'
+
+
+def test_usual_methods():
+ class A:
+ def method(self):
+ pass
+
+ class B:
+ def method(self, a, b):
+ pass
+
+ assert getsource(A().method).splitlines() == [' def method(self):', ' pass']
+ assert getsource(B().method).splitlines() == [' def method(self, a, b):', ' pass']
+
+
+def test_usual_classmethods():
+ class A:
+ @classmethod
+ def method(cls):
+ pass
+
+ class B:
+ @classmethod
+ def method(cls, a, b):
+ pass
+
+ assert getsource(A().method).splitlines() == [' @classmethod', ' def method(cls):', ' pass']
+ assert getsource(B().method).splitlines() == [' @classmethod', ' def method(cls, a, b):', ' pass']
+ assert getsource(A.method).splitlines() == [' @classmethod', ' def method(cls):', ' pass']
+ assert getsource(B.method).splitlines() == [' @classmethod', ' def method(cls, a, b):', ' pass']
+
+
+def test_usual_staticmethods():
+ class A:
+ @staticmethod
+ def method():
+ pass
+
+ class B:
+ @staticmethod
+ def method(a, b):
+ pass
+
+ assert getsource(A().method).splitlines() == [' @staticmethod', ' def method():', ' pass']
+ assert getsource(B().method).splitlines() == [' @staticmethod', ' def method(a, b):', ' pass']
+ assert getsource(A.method).splitlines() == [' @staticmethod', ' def method():', ' pass']
+ assert getsource(B.method).splitlines() == [' @staticmethod', ' def method(a, b):', ' pass']
+
+
+@pytest.mark.skipif(platform == "win32", reason='I wait this: https://github.com/raczben/wexpect/issues/55')
+def test_usual_functions_in_REPL(): # noqa: N802
+ from pexpect import spawn # type: ignore[import-untyped] # noqa: PLC0415
+
+ env = environ.copy()
+ env["PYTHON_COLORS"] = "0"
+ child = spawn('python3', ["-i"], encoding="utf-8", env=env, timeout=5)
+
+ buffer = StringIO()
+ child.logfile = buffer
+
+ child.expect(">>> ")
+ child.sendline('from getsources import getsource')
+ child.expect(">>> ")
+ child.sendline('def function(): ...')
+ child.sendline('')
+ child.expect(">>> ")
+
+ before = buffer.getvalue()
+
+ child.sendline("print(getsource(function), end='')")
+ child.expect(">>> ")
+
+ after = buffer.getvalue()
+ after = re.compile(r'(?:\x1B[@-_]|\x9B)[0-?]*[ -/]*[@-~]').sub('', after.lstrip(before))
+ after = ''.join(ch for ch in after if ch >= ' ' or ch in '\n\r\t')
+ after = after.splitlines()
+
+ child.sendline("exit()")
+
+ assert any('def function(): ...' in x for x in after)
+
+
+@pytest.mark.skipif(platform == "win32", reason='I wait this: https://github.com/raczben/wexpect/issues/55')
+def test_lambda_in_REPL(): # noqa: N802
+ from pexpect import spawn # type: ignore[import-untyped] # noqa: PLC0415
+
+ env = environ.copy()
+ env["PYTHON_COLORS"] = "0"
+ child = spawn('python3', ["-i"], encoding="utf-8", env=env, timeout=5)
+
+ buffer = StringIO()
+ child.logfile = buffer
+
+ child.expect(">>> ")
+ child.sendline('from getsources import getsource')
+ child.expect(">>> ")
+ child.sendline('function = lambda x: x')
+ child.expect(">>> ")
+
+ before = buffer.getvalue()
+
+ child.sendline("print(getsource(function), end='')")
+ child.expect(">>> ")
+
+ after = buffer.getvalue()
+ after = after[len(before):]
+ after = after.splitlines()
+
+ child.sendline("exit()")
+
+ assert any('function = lambda x: x' in x for x in after)
diff --git a/tests/test_clear.py b/tests/test_clear.py
new file mode 100644
index 0000000..95474b0
--- /dev/null
+++ b/tests/test_clear.py
@@ -0,0 +1,174 @@
+import re
+from io import StringIO
+from os import environ
+from sys import platform
+
+import pytest
+
+from getsources import getclearsource
+
+
+def global_function_1():
+ ...
+
+def global_function_2(a, b):
+ ...
+
+global_function_3 = lambda x: x
+
+class GlobalClass:
+ def simple_method(self):
+ pass
+
+ def method_with_parameters(self, a, b):
+ pass
+
+ @classmethod
+ def class_method(cls, a, b):
+ pass
+
+ @staticmethod
+ def static_method(a, b):
+ pass
+
+
+def test_usual_functions():
+ def function_1():
+ pass
+
+ def function_2(a, b):
+ pass
+
+ assert getclearsource(function_1).splitlines() == ['def function_1():', ' pass']
+ assert getclearsource(function_2).splitlines() == ['def function_2(a, b):', ' pass']
+
+ assert getclearsource(global_function_1).splitlines() == ['def global_function_1():', ' ...']
+ assert getclearsource(global_function_2).splitlines() == ['def global_function_2(a, b):', ' ...']
+
+
+def test_lambda():
+ function = lambda x: x
+
+ assert getclearsource(function) == 'function = lambda x: x'
+ assert getclearsource(global_function_3) == 'global_function_3 = lambda x: x'
+
+
+def test_usual_methods():
+ class A:
+ def method(self):
+ pass
+
+ class B:
+ def method(self, a, b):
+ pass
+
+ assert getclearsource(A().method).splitlines() == ['def method(self):', ' pass']
+ assert getclearsource(B().method).splitlines() == ['def method(self, a, b):', ' pass']
+
+ assert getclearsource(GlobalClass().simple_method).splitlines() == ['def simple_method(self):', ' pass']
+ assert getclearsource(GlobalClass().method_with_parameters).splitlines() == ['def method_with_parameters(self, a, b):', ' pass']
+
+
+def test_usual_classmethods():
+ class A:
+ @classmethod
+ def method(cls):
+ pass
+
+ class B:
+ @classmethod
+ def method(cls, a, b):
+ pass
+
+ assert getclearsource(A().method).splitlines() == ['@classmethod', 'def method(cls):', ' pass']
+ assert getclearsource(B().method).splitlines() == ['@classmethod', 'def method(cls, a, b):', ' pass']
+ assert getclearsource(A.method).splitlines() == ['@classmethod', 'def method(cls):', ' pass']
+ assert getclearsource(B.method).splitlines() == ['@classmethod', 'def method(cls, a, b):', ' pass']
+
+ assert getclearsource(GlobalClass().class_method).splitlines() == ['@classmethod', 'def class_method(cls, a, b):', ' pass']
+ assert getclearsource(GlobalClass.class_method).splitlines() == ['@classmethod', 'def class_method(cls, a, b):', ' pass']
+
+
+def test_usual_staticmethods():
+ class A:
+ @staticmethod
+ def method():
+ pass
+
+ class B:
+ @staticmethod
+ def method(a, b):
+ pass
+
+ assert getclearsource(A().method).splitlines() == ['@staticmethod', 'def method():', ' pass']
+ assert getclearsource(B().method).splitlines() == ['@staticmethod', 'def method(a, b):', ' pass']
+
+ assert getclearsource(A.method).splitlines() == ['@staticmethod', 'def method():', ' pass']
+ assert getclearsource(B.method).splitlines() == ['@staticmethod', 'def method(a, b):', ' pass']
+
+ assert getclearsource(GlobalClass().static_method).splitlines() == ['@staticmethod', 'def static_method(a, b):', ' pass']
+ assert getclearsource(GlobalClass.static_method).splitlines() == ['@staticmethod', 'def static_method(a, b):', ' pass']
+
+
+@pytest.mark.skipif(platform == "win32", reason='I wait this: https://github.com/raczben/wexpect/issues/55')
+def test_usual_functions_in_REPL(): # noqa: N802
+ from pexpect import spawn # type: ignore[import-untyped] # noqa: PLC0415
+
+ env = environ.copy()
+ env["PYTHON_COLORS"] = "0"
+ child = spawn('python3', ["-i"], encoding="utf-8", env=env, timeout=5)
+
+ buffer = StringIO()
+ child.logfile = buffer
+
+ child.expect(">>> ")
+ child.sendline('from getsources import getclearsource')
+ child.expect(">>> ")
+ child.sendline('def function(): ...')
+ child.sendline('')
+ child.expect(">>> ")
+
+ before = buffer.getvalue()
+
+ child.sendline("print(getclearsource(function), end='')")
+ child.expect(">>> ")
+
+ after = buffer.getvalue()
+ after = re.compile(r'(?:\x1B[@-_]|\x9B)[0-?]*[ -/]*[@-~]').sub('', after.lstrip(before))
+ after = ''.join(ch for ch in after if ch >= ' ' or ch in '\n\r\t')
+ after = after.splitlines()
+
+ child.sendline("exit()")
+
+ assert any('def function(): ...' in x for x in after)
+
+
+@pytest.mark.skipif(platform == "win32", reason='I wait this: https://github.com/raczben/wexpect/issues/55')
+def test_lambda_in_REPL(): # noqa: N802
+ from pexpect import spawn # type: ignore[import-untyped] # noqa: PLC0415
+
+ env = environ.copy()
+ env["PYTHON_COLORS"] = "0"
+ child = spawn('python3', ["-i"], encoding="utf-8", env=env, timeout=5)
+
+ buffer = StringIO()
+ child.logfile = buffer
+
+ child.expect(">>> ")
+ child.sendline('from getsources import getclearsource')
+ child.expect(">>> ")
+ child.sendline('function = lambda x: x')
+ child.expect(">>> ")
+
+ before = buffer.getvalue()
+
+ child.sendline("print(getclearsource(function), end='')")
+ child.expect(">>> ")
+
+ after = buffer.getvalue()
+ after = after[len(before):]
+ after = after.splitlines()
+
+ child.sendline("exit()")
+
+ assert any('function = lambda x: x' in x for x in after)