From de7b6461d75510eb8d817207f051ea640294e86a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 13:56:37 +0300 Subject: [PATCH 01/51] Add project templates, CI/CD workflows, and dev setup --- .github/ISSUE_TEMPLATE/bug_report.md | 32 +++++++++++++ .github/ISSUE_TEMPLATE/documentation.md | 26 +++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 17 +++++++ .github/ISSUE_TEMPLATE/question.md | 12 +++++ .github/workflows/lint.yml | 50 +++++++++++++++++++++ .github/workflows/release.yml | 34 ++++++++++++++ .github/workflows/tests_and_coverage.yml | 55 +++++++++++++++++++++++ .gitignore | 14 ++++++ README.md | 3 +- getsources/__init__.py | 0 getsources/py.typed | 0 pyproject.toml | 52 +++++++++++++++++++++ requirements_dev.txt | 9 ++++ tests/__init__.py | 0 14 files changed, 302 insertions(+), 2 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/documentation.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/question.md create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/tests_and_coverage.yml create mode 100644 .gitignore create mode 100644 getsources/__init__.py create mode 100644 getsources/py.typed create mode 100644 pyproject.toml create mode 100644 requirements_dev.txt create mode 100644 tests/__init__.py 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..7ace093 --- /dev/null +++ b/.github/workflows/tests_and_coverage.yml @@ -0,0 +1,55 @@ +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 + + - 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..cb78615 100644 --- a/README.md +++ b/README.md @@ -1,2 +1 @@ -# getsource -A way to get the source code of functions +# getsources: a way to get the source code of functions diff --git a/getsources/__init__.py b/getsources/__init__.py new file mode 100644 index 0000000..e69de29 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..6d4ac95 --- /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'] +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..8326d2a --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,9 @@ +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 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 From 99ad60f9c61895448d3a36876c839eb195a4c24d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 14:43:33 +0300 Subject: [PATCH 02/51] Add getsource function with fallback to dill --- getsources/__init__.py | 1 + getsources/function.py | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 getsources/function.py diff --git a/getsources/__init__.py b/getsources/__init__.py index e69de29..aad4fbd 100644 --- a/getsources/__init__.py +++ b/getsources/__init__.py @@ -0,0 +1 @@ +from getsources.function import getsource diff --git a/getsources/function.py b/getsources/function.py new file mode 100644 index 0000000..6ba0ecd --- /dev/null +++ b/getsources/function.py @@ -0,0 +1,10 @@ +from typing import Callable, Any +from inspect import getsource as original_getsource +from dill.source import getsource as dill_getsource + + +def getsource(function: Callable[..., Any]) -> str: + try: + return original_getsource(function) + except OSError: + return dill_getsource(function) From c3d6b0832fedcfcef7b7a6f8868eb7489fa3dcc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 14:44:00 +0300 Subject: [PATCH 03/51] Add tests for getsource function behavior --- tests/test_function.py | 61 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/test_function.py diff --git a/tests/test_function.py b/tests/test_function.py new file mode 100644 index 0000000..138ca3e --- /dev/null +++ b/tests/test_function.py @@ -0,0 +1,61 @@ +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'] + + +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'] From 618a663a71eb8ca917fc727ae7fa6eca36f2c253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 14:45:13 +0300 Subject: [PATCH 04/51] Move getsource() to "base" module --- getsources/__init__.py | 2 +- getsources/{function.py => base.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename getsources/{function.py => base.py} (100%) diff --git a/getsources/__init__.py b/getsources/__init__.py index aad4fbd..e34d3df 100644 --- a/getsources/__init__.py +++ b/getsources/__init__.py @@ -1 +1 @@ -from getsources.function import getsource +from getsources.base import getsource diff --git a/getsources/function.py b/getsources/base.py similarity index 100% rename from getsources/function.py rename to getsources/base.py From d14b717e63338076a1c5c5eb6de375683d91243d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 14:52:43 +0300 Subject: [PATCH 05/51] Add README with project purpose --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index cb78615..3746d47 100644 --- a/README.md +++ b/README.md @@ -1 +1,4 @@ # getsources: a way to get the source code of functions + + +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). From 49257b90142428d8753aba0d7646886010755557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 14:53:14 +0300 Subject: [PATCH 06/51] Rename test_function.py to test_base.py --- tests/{test_function.py => test_base.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_function.py => test_base.py} (100%) diff --git a/tests/test_function.py b/tests/test_base.py similarity index 100% rename from tests/test_function.py rename to tests/test_base.py From bcdb7be30f28f215e54cf3fe7262ec5140d2cf4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 15:10:47 +0300 Subject: [PATCH 07/51] Add tests for REPL functions and lambdas --- tests/test_base.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/test_base.py b/tests/test_base.py index 138ca3e..cd9c234 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,3 +1,10 @@ +import code +from sys import version_info +from contextlib import redirect_stdout +from io import StringIO + +import pytest + from getsources import getsource @@ -59,3 +66,36 @@ def method(a, b): assert getsource(A().method).splitlines() == [' @staticmethod', ' def method():', ' pass'] assert getsource(B().method).splitlines() == [' @staticmethod', ' def method(a, b):', ' pass'] + + +@pytest.mark.skipif(version_info >= (3, 14), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') +def test_usual_functions_in_REPL(): + console = code.InteractiveConsole({}) + buffer = StringIO() + + console.push("from getsources import getsource") + console.push("def function(): pass") + console.push("") + + with redirect_stdout(buffer): + console.push("print(getsource(function), end='')") + + assert buffer.getvalue() == 'def function(): pass' + + +@pytest.mark.skipif(version_info >= (3, 14), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') +def test_lambda_in_REPL(): + function = lambda x: x + + assert getsource(function).strip() == 'function = lambda x: x' + + console = code.InteractiveConsole({}) + buffer = StringIO() + + console.push("from getsources import getsource") + console.push('function = lambda x: x') + + with redirect_stdout(buffer): + console.push("print(getsource(function), end='')") + + assert buffer.getvalue() == 'function = lambda x: x' From 073dde517dcd69efe7535a2f92703a94bf0b5d4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 15:15:56 +0300 Subject: [PATCH 08/51] Allow uploading of the test coverage to fail without breaking workflow --- .github/workflows/tests_and_coverage.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests_and_coverage.yml b/.github/workflows/tests_and_coverage.yml index 7ace093..46737ad 100644 --- a/.github/workflows/tests_and_coverage.yml +++ b/.github/workflows/tests_and_coverage.yml @@ -50,6 +50,7 @@ jobs: 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 From 3f167a906c0d3ac01765de421122f892a2fb7e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 15:19:33 +0300 Subject: [PATCH 09/51] Lints' issues --- getsources/__init__.py | 2 +- getsources/base.py | 7 ++++--- pyproject.toml | 2 +- tests/test_base.py | 6 +++--- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/getsources/__init__.py b/getsources/__init__.py index e34d3df..b3a71bc 100644 --- a/getsources/__init__.py +++ b/getsources/__init__.py @@ -1 +1 @@ -from getsources.base import getsource +from getsources.base import getsource as getsource diff --git a/getsources/base.py b/getsources/base.py index 6ba0ecd..6701ba2 100644 --- a/getsources/base.py +++ b/getsources/base.py @@ -1,10 +1,11 @@ -from typing import Callable, Any from inspect import getsource as original_getsource -from dill.source import getsource as dill_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: - return dill_getsource(function) + return dill_getsource(function) # type: ignore[no-any-return] diff --git a/pyproject.toml b/pyproject.toml index 6d4ac95..5d8d5de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ keywords = ['inspect'] markers = ["mypy_testing"] [tool.ruff] -lint.ignore = ['E501', 'E712', 'PTH123', 'PTH118', 'PLR2004', 'PTH107', 'SIM105', 'SIM102', 'RET503', 'PLR0912', 'C901'] +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" diff --git a/tests/test_base.py b/tests/test_base.py index cd9c234..0f37249 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,7 +1,7 @@ import code -from sys import version_info from contextlib import redirect_stdout from io import StringIO +from sys import version_info import pytest @@ -69,7 +69,7 @@ def method(a, b): @pytest.mark.skipif(version_info >= (3, 14), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') -def test_usual_functions_in_REPL(): +def test_usual_functions_in_REPL(): # noqa: N802 console = code.InteractiveConsole({}) buffer = StringIO() @@ -84,7 +84,7 @@ def test_usual_functions_in_REPL(): @pytest.mark.skipif(version_info >= (3, 14), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') -def test_lambda_in_REPL(): +def test_lambda_in_REPL(): # noqa: N802 function = lambda x: x assert getsource(function).strip() == 'function = lambda x: x' From 93a09fd51911173f21fd1fd57bd2cd5214b7bc38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 15:27:44 +0300 Subject: [PATCH 10/51] Update Python version condition for dill REPL tests --- tests/test_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index 0f37249..fea1cca 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -68,7 +68,7 @@ def method(a, b): assert getsource(B().method).splitlines() == [' @staticmethod', ' def method(a, b):', ' pass'] -@pytest.mark.skipif(version_info >= (3, 14), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') +@pytest.mark.skipif(version_info >= (3, 11), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') def test_usual_functions_in_REPL(): # noqa: N802 console = code.InteractiveConsole({}) buffer = StringIO() @@ -83,7 +83,7 @@ def test_usual_functions_in_REPL(): # noqa: N802 assert buffer.getvalue() == 'def function(): pass' -@pytest.mark.skipif(version_info >= (3, 14), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') +@pytest.mark.skipif(version_info >= (3, 11), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') def test_lambda_in_REPL(): # noqa: N802 function = lambda x: x From a0fd0ed084c23f1e005eb7f6889740fa15acf176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 15:31:38 +0300 Subject: [PATCH 11/51] Add pragma to suppress coverage warning for OSError fallback --- getsources/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/getsources/base.py b/getsources/base.py index 6701ba2..0c99557 100644 --- a/getsources/base.py +++ b/getsources/base.py @@ -7,5 +7,5 @@ def getsource(function: Callable[..., Any]) -> str: try: return original_getsource(function) - except OSError: + except OSError: # pragma: no cover return dill_getsource(function) # type: ignore[no-any-return] From 3add8de850543fe540bde98c3bf0be4c91fd4a65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 15:36:26 +0300 Subject: [PATCH 12/51] Update skip condition for Python 3.11 compatibility --- tests/test_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index fea1cca..6249d9f 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -68,7 +68,7 @@ def method(a, b): assert getsource(B().method).splitlines() == [' @staticmethod', ' def method(a, b):', ' pass'] -@pytest.mark.skipif(version_info >= (3, 11), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') +@pytest.mark.skipif(version_info >= (3, 10), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') def test_usual_functions_in_REPL(): # noqa: N802 console = code.InteractiveConsole({}) buffer = StringIO() @@ -83,7 +83,7 @@ def test_usual_functions_in_REPL(): # noqa: N802 assert buffer.getvalue() == 'def function(): pass' -@pytest.mark.skipif(version_info >= (3, 11), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') +@pytest.mark.skipif(version_info >= (3, 10), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') def test_lambda_in_REPL(): # noqa: N802 function = lambda x: x From db3020d74edb7478d7ae1b1d841153877fe45649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 15:39:05 +0300 Subject: [PATCH 13/51] Update skip condition for dill issue from Python 3.10 to 3.9 --- tests/test_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index 6249d9f..0b5bab4 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -68,7 +68,7 @@ def method(a, b): assert getsource(B().method).splitlines() == [' @staticmethod', ' def method(a, b):', ' pass'] -@pytest.mark.skipif(version_info >= (3, 10), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') +@pytest.mark.skipif(version_info >= (3, 9), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') def test_usual_functions_in_REPL(): # noqa: N802 console = code.InteractiveConsole({}) buffer = StringIO() @@ -83,7 +83,7 @@ def test_usual_functions_in_REPL(): # noqa: N802 assert buffer.getvalue() == 'def function(): pass' -@pytest.mark.skipif(version_info >= (3, 10), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') +@pytest.mark.skipif(version_info >= (3, 9), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') def test_lambda_in_REPL(): # noqa: N802 function = lambda x: x From 1692a5bb101f4d557bd16e4e7619afe545d63b51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 17:21:04 +0300 Subject: [PATCH 14/51] Use pexpect to test REPL function source output --- tests/test_base.py | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index 0b5bab4..abdddff 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,9 +1,12 @@ +import re import code +from os import environ from contextlib import redirect_stdout from io import StringIO from sys import version_info import pytest +from pexpect import spawn from getsources import getsource @@ -68,19 +71,36 @@ def method(a, b): assert getsource(B().method).splitlines() == [' @staticmethod', ' def method(a, b):', ' pass'] -@pytest.mark.skipif(version_info >= (3, 9), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') -def test_usual_functions_in_REPL(): # noqa: N802 - console = code.InteractiveConsole({}) +#@pytest.mark.skipif(version_info >= (3, 9), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') +def test_usual_functions_in_REPL(): + env = environ.copy() + env["PYTHON_COLORS"] = "0" + child = spawn('python3', ["-i"], encoding="utf-8", env=env, timeout=5) + buffer = StringIO() + child.logfile = buffer - console.push("from getsources import getsource") - console.push("def function(): pass") - console.push("") + child.setecho(False) + child.expect(">>> ") + child.sendline('from getsources import getsource') + child.expect(">>> ") + child.sendline('def function(): ...') + child.sendline('') + child.expect(">>> ") - with redirect_stdout(buffer): - console.push("print(getsource(function), end='')") + before = buffer.getvalue() + + child.sendline("print(getsource(function), end='')") + child.expect(">>> ") + + after = re.compile(r'(?:\x1B[@-_]|\x9B)[0-?]*[ -/]*[@-~]').sub('', buffer.getvalue().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) - assert buffer.getvalue() == 'def function(): pass' @pytest.mark.skipif(version_info >= (3, 9), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') From ba9918e606b4871aade6a6fe51e8274fef89c6dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 17:21:58 +0300 Subject: [PATCH 15/51] Add pexpect==4.9.0 to requirements_dev.txt --- requirements_dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_dev.txt b/requirements_dev.txt index 8326d2a..30d7925 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -7,3 +7,4 @@ ruff==0.14.6 pytest-mypy-testing==0.1.3 full_match==0.0.3 transfunctions==0.0.9 +pexpect==4.9.0 From 8c8a79f9cdb50fe65e33c0a101c3ceed771df2c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 17:29:52 +0300 Subject: [PATCH 16/51] Add wexpect as fallback for pexpect --- requirements_dev.txt | 1 + tests/test_base.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 30d7925..2664b51 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -8,3 +8,4 @@ pytest-mypy-testing==0.1.3 full_match==0.0.3 transfunctions==0.0.9 pexpect==4.9.0 +wexpect==2.3.2 diff --git a/tests/test_base.py b/tests/test_base.py index abdddff..3790b82 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -6,7 +6,10 @@ from sys import version_info import pytest -from pexpect import spawn +try: + from pexpect import spawn +except ImportError: + from wexpect import spawn from getsources import getsource From 4df41dcb46e62a5fe5cb6cee080c6d8f0afb5c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 17:38:30 +0300 Subject: [PATCH 17/51] Update platform-specific pexpect/wexpect versions --- requirements_dev.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 2664b51..e7fbdc0 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -7,5 +7,5 @@ ruff==0.14.6 pytest-mypy-testing==0.1.3 full_match==0.0.3 transfunctions==0.0.9 -pexpect==4.9.0 -wexpect==2.3.2 +pexpect==4.9.0; sys_platform == "linux" or sys_platform == "darwin" +wexpect==4.0.0; sys_platform == "windows" From fb818e8511c09184c0ffcf2e64c8590642eef2b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 17:42:38 +0300 Subject: [PATCH 18/51] Add test_lambda_in_REPL with skip for Python 3.9+ --- tests/test_base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_base.py b/tests/test_base.py index 3790b82..d901379 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -105,7 +105,7 @@ def test_usual_functions_in_REPL(): assert any('def function(): ...' in x for x in after) - +""" @pytest.mark.skipif(version_info >= (3, 9), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') def test_lambda_in_REPL(): # noqa: N802 function = lambda x: x @@ -122,3 +122,4 @@ def test_lambda_in_REPL(): # noqa: N802 console.push("print(getsource(function), end='')") assert buffer.getvalue() == 'function = lambda x: x' +""" From abfbd50ea63f115deccfcb6e5218c13191c984ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 17:45:13 +0300 Subject: [PATCH 19/51] Update pexpect platform condition to use "win32" on Windows --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index e7fbdc0..a964b3e 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -8,4 +8,4 @@ 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" -wexpect==4.0.0; sys_platform == "windows" +wexpect==4.0.0; sys_platform == "win32" From 87a556406f0c2c7ef5160e32328def92ebd1154f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 17:49:39 +0300 Subject: [PATCH 20/51] Disable echo in REPL test to avoid side effects --- tests/test_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_base.py b/tests/test_base.py index d901379..45f0df2 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -83,7 +83,7 @@ def test_usual_functions_in_REPL(): buffer = StringIO() child.logfile = buffer - child.setecho(False) + #child.setecho(False) child.expect(">>> ") child.sendline('from getsources import getsource') child.expect(">>> ") From a4f2c6111020ad19df41b6fc80b0c9fc46b43606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 18:36:48 +0300 Subject: [PATCH 21/51] Add setuptools==82.0.0 to requirements_dev.txt --- requirements_dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_dev.txt b/requirements_dev.txt index a964b3e..cba3d66 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -9,3 +9,4 @@ full_match==0.0.3 transfunctions==0.0.9 pexpect==4.9.0; sys_platform == "linux" or sys_platform == "darwin" wexpect==4.0.0; sys_platform == "win32" +setuptools==82.0.0 From 6bd8da277d4102fd9075cb9177d0b1598db92079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 18:42:30 +0300 Subject: [PATCH 22/51] Downgrade setuptools from 82.0.0 to 75.3.4 --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index cba3d66..c4acf66 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -9,4 +9,4 @@ full_match==0.0.3 transfunctions==0.0.9 pexpect==4.9.0; sys_platform == "linux" or sys_platform == "darwin" wexpect==4.0.0; sys_platform == "win32" -setuptools==82.0.0 +setuptools==75.3.4 From 94bb017981a24a06589c82bf7bc2ac8dc6953792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 18:46:11 +0300 Subject: [PATCH 23/51] Add print(after) to test_usual_functions_in_REPL --- tests/test_base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_base.py b/tests/test_base.py index 45f0df2..e2a399c 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -102,6 +102,8 @@ def test_usual_functions_in_REPL(): child.sendline("exit()") + print(after) + assert any('def function(): ...' in x for x in after) From 548f71a608ac87a5515c83e775cdec6bbb0274eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 18:52:22 +0300 Subject: [PATCH 24/51] Fix REPL test by preserving buffer content before sanitization --- tests/test_base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_base.py b/tests/test_base.py index e2a399c..2015598 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -96,7 +96,10 @@ def test_usual_functions_in_REPL(): child.sendline("print(getsource(function), end='')") child.expect(">>> ") - after = re.compile(r'(?:\x1B[@-_]|\x9B)[0-?]*[ -/]*[@-~]').sub('', buffer.getvalue().lstrip(before)) + after = buffer.getvalue() + print(after) + print('-------------') + 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() From f76e1f33b23582b84a45c242c0f8fd514e19f638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 18:53:11 +0300 Subject: [PATCH 25/51] Disable ANSI escape code stripping in test_base.py --- tests/test_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index 2015598..0b69a36 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -99,8 +99,8 @@ def test_usual_functions_in_REPL(): after = buffer.getvalue() print(after) print('-------------') - 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 = 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()") From 31e397cd8c18b7205e7a4b3e9b0ad909122774b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 19:09:59 +0300 Subject: [PATCH 26/51] Update test to skip on Windows platform --- requirements_dev.txt | 1 - tests/test_base.py | 14 ++++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index c4acf66..0da7190 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -8,5 +8,4 @@ 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" -wexpect==4.0.0; sys_platform == "win32" setuptools==75.3.4 diff --git a/tests/test_base.py b/tests/test_base.py index 0b69a36..1747195 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -3,13 +3,9 @@ from os import environ from contextlib import redirect_stdout from io import StringIO -from sys import version_info +from sys import version_info, platform import pytest -try: - from pexpect import spawn -except ImportError: - from wexpect import spawn from getsources import getsource @@ -74,8 +70,10 @@ def method(a, b): assert getsource(B().method).splitlines() == [' @staticmethod', ' def method(a, b):', ' pass'] -#@pytest.mark.skipif(version_info >= (3, 9), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') +@pytest.mark.skipif(platform == "win32", reason='I wait this: https://github.com/raczben/wexpect/issues/55') def test_usual_functions_in_REPL(): + from pexpect import spawn + env = environ.copy() env["PYTHON_COLORS"] = "0" child = spawn('python3', ["-i"], encoding="utf-8", env=env, timeout=5) @@ -99,8 +97,8 @@ def test_usual_functions_in_REPL(): after = buffer.getvalue() print(after) print('-------------') - #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 = 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()") From 8e4108bebe1543af39655b8fe31191fae2491462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 19:19:29 +0300 Subject: [PATCH 27/51] Remove commented setecho(False) and print statements from test_base.py --- tests/test_base.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index 1747195..3678360 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -81,7 +81,6 @@ def test_usual_functions_in_REPL(): buffer = StringIO() child.logfile = buffer - #child.setecho(False) child.expect(">>> ") child.sendline('from getsources import getsource') child.expect(">>> ") @@ -95,16 +94,12 @@ def test_usual_functions_in_REPL(): child.expect(">>> ") after = buffer.getvalue() - print(after) - print('-------------') 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()") - print(after) - assert any('def function(): ...' in x for x in after) From 2035a5df09b50d345513d5a2ec087466bac80167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 19:21:46 +0300 Subject: [PATCH 28/51] Lints' issues --- tests/test_base.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index 3678360..9e4c8de 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,9 +1,7 @@ import re -import code -from os import environ -from contextlib import redirect_stdout from io import StringIO -from sys import version_info, platform +from os import environ +from sys import platform import pytest @@ -71,8 +69,8 @@ def method(a, b): @pytest.mark.skipif(platform == "win32", reason='I wait this: https://github.com/raczben/wexpect/issues/55') -def test_usual_functions_in_REPL(): - from pexpect import spawn +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" From 0a156daad64b01b2ed0a21d0f66008a11a9910c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 19:40:23 +0300 Subject: [PATCH 29/51] A logo --- README.md | 3 +- docs/assets/logo_1.svg | 86 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 docs/assets/logo_1.svg diff --git a/README.md b/README.md index 3746d47..89aa65b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -# getsources: a way to get the source code of functions - +![logo](https://raw.githubusercontent.com/mutating/getsources/develop/docs/assets/logo_1.svg) 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). diff --git a/docs/assets/logo_1.svg b/docs/assets/logo_1.svg new file mode 100644 index 0000000..5240c82 --- /dev/null +++ b/docs/assets/logo_1.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + GETSOURCE + From 83beca49df232c09688142d30aea15c077b7b579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 19:44:13 +0300 Subject: [PATCH 30/51] Add project badges to README --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 89aa65b..7618aec 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,21 @@ +
+ + +[![Downloads](https://static.pepy.tech/badge/getsources/month)](https://pepy.tech/project/getsources) +[![Downloads](https://static.pepy.tech/badge/getsources)](https://pepy.tech/project/getsources) +[![Coverage Status](https://coveralls.io/repos/github/mutating/getsources/badge.svg?branch=main)](https://coveralls.io/github/mutating/getsources?branch=main) +[![Lines of code](https://sloc.xyz/github/mutating/getsources/?category=code)](https://github.com/boyter/scc/) +[![Hits-of-Code](https://hitsofcode.com/github/mutating/getsources?branch=main)](https://hitsofcode.com/github/mutating/getsources/view?branch=main) +[![Test-Package](https://github.com/mutating/getsources/actions/workflows/tests_and_coverage.yml/badge.svg)](https://github.com/mutating/getsources/actions/workflows/tests_and_coverage.yml) +[![Python versions](https://img.shields.io/pypi/pyversions/getsources.svg)](https://pypi.python.org/pypi/getsources) +[![PyPI version](https://badge.fury.io/py/getsources.svg)](https://badge.fury.io/py/getsources) +[![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/mutating/getsources) + +
+ ![logo](https://raw.githubusercontent.com/mutating/getsources/develop/docs/assets/logo_1.svg) + 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). From a318d08a3670a3c637c64cceaece188652f172c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 19:46:56 +0300 Subject: [PATCH 31/51] The logo fixed --- docs/assets/logo_1.svg | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/docs/assets/logo_1.svg b/docs/assets/logo_1.svg index 5240c82..f841438 100644 --- a/docs/assets/logo_1.svg +++ b/docs/assets/logo_1.svg @@ -1,9 +1,9 @@ - GETSOURCE + From 430419e2cadbc50c411d291a9bfdfa8388e56caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 19:53:18 +0300 Subject: [PATCH 32/51] Add installation and usage documentation for getsources --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index 7618aec..886b052 100644 --- a/README.md +++ b/README.md @@ -19,3 +19,35 @@ 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 From 826c1bb36f9a15e4caebee1dbf14b6d8e35e01ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 19:56:36 +0300 Subject: [PATCH 33/51] Remove skipped test for lambda in REPL --- tests/test_base.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index 9e4c8de..c2bd5c7 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -99,23 +99,3 @@ def test_usual_functions_in_REPL(): # noqa: N802 child.sendline("exit()") assert any('def function(): ...' in x for x in after) - - -""" -@pytest.mark.skipif(version_info >= (3, 9), reason='I wait this: https://github.com/uqfoundation/dill/issues/745') -def test_lambda_in_REPL(): # noqa: N802 - function = lambda x: x - - assert getsource(function).strip() == 'function = lambda x: x' - - console = code.InteractiveConsole({}) - buffer = StringIO() - - console.push("from getsources import getsource") - console.push('function = lambda x: x') - - with redirect_stdout(buffer): - console.push("print(getsource(function), end='')") - - assert buffer.getvalue() == 'function = lambda x: x' -""" From 5edd0faf94dcc47dd3c4dafbf088fe20621fd9b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 19:57:58 +0300 Subject: [PATCH 34/51] Add test for lambda in REPL --- tests/test_base.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_base.py b/tests/test_base.py index c2bd5c7..9ce6219 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -99,3 +99,35 @@ def test_usual_functions_in_REPL(): # noqa: N802 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 = 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('function = lambda x: x' in x for x in after) From 6fce6293295dda70af3b7d8dc1c05e15e63e2766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 20:13:37 +0300 Subject: [PATCH 35/51] Add getclearsource to expose stripped source utility --- getsources/__init__.py | 1 + getsources/clear.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 getsources/clear.py diff --git a/getsources/__init__.py b/getsources/__init__.py index b3a71bc..ca36d06 100644 --- a/getsources/__init__.py +++ b/getsources/__init__.py @@ -1 +1,2 @@ from getsources.base import getsource as getsource +from getsources.clear import getclearsource as getclearsource diff --git a/getsources/clear.py b/getsources/clear.py new file mode 100644 index 0000000..fa176e1 --- /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]: + 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') From 6c4bfbc81b7ef724d419e62224bb164ccd2d6216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 20:15:14 +0300 Subject: [PATCH 36/51] Add tests for classmethod and staticmethod source retrieval via class reference --- tests/test_base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_base.py b/tests/test_base.py index 9ce6219..8a76049 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -51,6 +51,8 @@ def method(cls, a, b): 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(): @@ -66,6 +68,8 @@ def method(a, b): 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') From a34b23452796eb944052d8051b104d9a8dd49f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 20:20:03 +0300 Subject: [PATCH 37/51] Add test suite for getclearsource functionality --- tests/test_clear.py | 175 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 tests/test_clear.py diff --git a/tests/test_clear.py b/tests/test_clear.py new file mode 100644 index 0000000..a80da57 --- /dev/null +++ b/tests/test_clear.py @@ -0,0 +1,175 @@ +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 = 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('function = lambda x: x' in x for x in after) From c3481203386127256094f1aee698c17dedbfc80d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 20:25:17 +0300 Subject: [PATCH 38/51] Add getclearsource function for trimmed source output --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 886b052..a51b5f8 100644 --- a/README.md +++ b/README.md @@ -51,3 +51,20 @@ 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`. From d27a9185e4aa53b0edba7be6350de5960530df21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 20:26:25 +0300 Subject: [PATCH 39/51] Add debug print to inspect buffer content in test_clear.py --- tests/test_clear.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_clear.py b/tests/test_clear.py index a80da57..220a673 100644 --- a/tests/test_clear.py +++ b/tests/test_clear.py @@ -166,6 +166,7 @@ def test_lambda_in_REPL(): # noqa: N802 child.expect(">>> ") after = buffer.getvalue() + print(after) 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() From b6a416d980653a2673cff70fc5f172d118e84040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 20:29:14 +0300 Subject: [PATCH 40/51] Add "# noqa: T201" to print statement in test_clear.py --- tests/test_clear.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_clear.py b/tests/test_clear.py index 220a673..349773d 100644 --- a/tests/test_clear.py +++ b/tests/test_clear.py @@ -166,7 +166,7 @@ def test_lambda_in_REPL(): # noqa: N802 child.expect(">>> ") after = buffer.getvalue() - print(after) + print(after) # noqa: T201 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() From 8bbd73a959588e34333050c797fe9bb5e8c1ae77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 20:29:29 +0300 Subject: [PATCH 41/51] Fix indentation calculation by adding pragma to suppress flake8 warning --- getsources/clear.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/getsources/clear.py b/getsources/clear.py index fa176e1..4911f82 100644 --- a/getsources/clear.py +++ b/getsources/clear.py @@ -9,7 +9,7 @@ def getclearsource(function: Callable[..., Any]) -> str: splitted_source_code = source_code.split('\n') indent = 0 - for letter in splitted_source_code[0]: + for letter in splitted_source_code[0]: # pragma: no branch if letter.isspace(): indent += 1 else: From 1688814857d36f580112acf1010ac211d8aee867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 20:32:50 +0300 Subject: [PATCH 42/51] Disable output filtering in test_clear.py --- tests/test_clear.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_clear.py b/tests/test_clear.py index 349773d..6a94b1c 100644 --- a/tests/test_clear.py +++ b/tests/test_clear.py @@ -167,8 +167,8 @@ def test_lambda_in_REPL(): # noqa: N802 after = buffer.getvalue() print(after) # noqa: T201 - 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 = 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()") From e2f79afdb0a40c585664cefa1e9d996c0e8dc6ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 20:34:22 +0300 Subject: [PATCH 43/51] Disable ANSI escape code stripping in test_base.py --- tests/test_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index 8a76049..576cb4e 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -128,8 +128,8 @@ def test_lambda_in_REPL(): # noqa: N802 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 = 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()") From 81c541c9dfbe945be427fa7500cbdcc6570728e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 20:38:22 +0300 Subject: [PATCH 44/51] Simplify test output processing by removing redundant stripping and filtering --- tests/test_base.py | 4 +--- tests/test_clear.py | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index 576cb4e..a9fcfb3 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -127,9 +127,7 @@ def test_lambda_in_REPL(): # noqa: N802 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 = buffer.getvalue().lstrip(before) after = after.splitlines() child.sendline("exit()") diff --git a/tests/test_clear.py b/tests/test_clear.py index 6a94b1c..576e798 100644 --- a/tests/test_clear.py +++ b/tests/test_clear.py @@ -165,10 +165,7 @@ def test_lambda_in_REPL(): # noqa: N802 child.sendline("print(getclearsource(function), end='')") child.expect(">>> ") - after = buffer.getvalue() - print(after) # noqa: T201 - #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 = buffer.getvalue().lstrip(before) after = after.splitlines() child.sendline("exit()") From 10ab4fb152af52bec114848dc446939a50fb4c3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 20:41:43 +0300 Subject: [PATCH 45/51] Update lambda assertion to exclude variable name in tests --- tests/test_base.py | 2 +- tests/test_clear.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index a9fcfb3..136a564 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -132,4 +132,4 @@ def test_lambda_in_REPL(): # noqa: N802 child.sendline("exit()") - assert any('function = lambda x: x' in x for x in after) + assert any('lambda x: x' in x for x in after) diff --git a/tests/test_clear.py b/tests/test_clear.py index 576e798..880434b 100644 --- a/tests/test_clear.py +++ b/tests/test_clear.py @@ -170,4 +170,4 @@ def test_lambda_in_REPL(): # noqa: N802 child.sendline("exit()") - assert any('function = lambda x: x' in x for x in after) + assert any('lambda x: x' in x for x in after) From f6720a3d5497d69057ced4831a97c17f20bb855c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 20:48:08 +0300 Subject: [PATCH 46/51] Add debug prints to test_lambda_in_REPL for easier inspection --- tests/test_base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_base.py b/tests/test_base.py index 136a564..20bb648 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -124,11 +124,17 @@ def test_lambda_in_REPL(): # noqa: N802 before = buffer.getvalue() + print(before) # noqa: T201 + print('------------') # noqa: T201 + child.sendline("print(getsource(function), end='')") child.expect(">>> ") after = buffer.getvalue().lstrip(before) + print(after) # noqa: T201 + print('------------') # noqa: T201 after = after.splitlines() + print(after) # noqa: T201 child.sendline("exit()") From 61e3a8de247022d00eeb061988d3eae2286b62fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 20:52:54 +0300 Subject: [PATCH 47/51] Add debug prints to test_lambda_in_REPL to aid in debugging --- tests/test_base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_base.py b/tests/test_base.py index 20bb648..acb8370 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -130,7 +130,10 @@ def test_lambda_in_REPL(): # noqa: N802 child.sendline("print(getsource(function), end='')") child.expect(">>> ") - after = buffer.getvalue().lstrip(before) + after = buffer.getvalue() + print(after) # noqa: T201 + print('------------') # noqa: T201 + after = after.lstrip(before) print(after) # noqa: T201 print('------------') # noqa: T201 after = after.splitlines() From bc1e1b355e197dcf7753cf210b0fc3a74dee1ffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 20:53:49 +0300 Subject: [PATCH 48/51] Update assertion to match actual lambda output in REPL test --- tests/test_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_base.py b/tests/test_base.py index acb8370..ed99836 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -141,4 +141,4 @@ def test_lambda_in_REPL(): # noqa: N802 child.sendline("exit()") - assert any('lambda x: x' in x for x in after) + assert any('function = lambda x: x' in x for x in after) From de78e04b0f832c5f22785327170bd393e17b0807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 20:56:20 +0300 Subject: [PATCH 49/51] Clean up test output by removing ANSI escape sequences and non-printable characters --- tests/test_base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_base.py b/tests/test_base.py index ed99836..df19d74 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -133,6 +133,10 @@ def test_lambda_in_REPL(): # noqa: N802 after = buffer.getvalue() print(after) # noqa: T201 print('------------') # noqa: T201 + 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') + print(after) # noqa: T201 + print('------------') # noqa: T201 after = after.lstrip(before) print(after) # noqa: T201 print('------------') # noqa: T201 From 01accd7e0411d30839372d6a0f3a9c616f061da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 20:59:50 +0300 Subject: [PATCH 50/51] Remove commented ANSI escape and whitespace filtering logic --- tests/test_base.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index df19d74..f0cca74 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -133,11 +133,9 @@ def test_lambda_in_REPL(): # noqa: N802 after = buffer.getvalue() print(after) # noqa: T201 print('------------') # noqa: T201 - 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') - print(after) # noqa: T201 - print('------------') # noqa: T201 - after = after.lstrip(before) + #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[len(before):] print(after) # noqa: T201 print('------------') # noqa: T201 after = after.splitlines() From 6092052ec649bffedce9f246ee71d5af05f68090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 25 Feb 2026 21:02:11 +0300 Subject: [PATCH 51/51] Update test to match output of getclearsource Fix assertion to match actual output from getclearsource --- tests/test_base.py | 10 ---------- tests/test_clear.py | 5 +++-- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index f0cca74..cc48b3b 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -124,22 +124,12 @@ def test_lambda_in_REPL(): # noqa: N802 before = buffer.getvalue() - print(before) # noqa: T201 - print('------------') # noqa: T201 - child.sendline("print(getsource(function), end='')") child.expect(">>> ") after = buffer.getvalue() - print(after) # noqa: T201 - print('------------') # noqa: T201 - #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[len(before):] - print(after) # noqa: T201 - print('------------') # noqa: T201 after = after.splitlines() - print(after) # noqa: T201 child.sendline("exit()") diff --git a/tests/test_clear.py b/tests/test_clear.py index 880434b..95474b0 100644 --- a/tests/test_clear.py +++ b/tests/test_clear.py @@ -165,9 +165,10 @@ def test_lambda_in_REPL(): # noqa: N802 child.sendline("print(getclearsource(function), end='')") child.expect(">>> ") - after = buffer.getvalue().lstrip(before) + after = buffer.getvalue() + after = after[len(before):] after = after.splitlines() child.sendline("exit()") - assert any('lambda x: x' in x for x in after) + assert any('function = lambda x: x' in x for x in after)