pax_global_header00006660000000000000000000000064146442150640014520gustar00rootroot0000000000000052 comment=d59b84224986167736f272b69d168a96b7bb5e73 python-makefun-1.15.4/000077500000000000000000000000001464421506400145555ustar00rootroot00000000000000python-makefun-1.15.4/.github/000077500000000000000000000000001464421506400161155ustar00rootroot00000000000000python-makefun-1.15.4/.github/workflows/000077500000000000000000000000001464421506400201525ustar00rootroot00000000000000python-makefun-1.15.4/.github/workflows/base.yml000066400000000000000000000160371464421506400216160ustar00rootroot00000000000000# .github/workflows/base.yml name: Build on: # this one is to trigger the workflow manually from the interface workflow_dispatch: push: tags: - '*' branches: - main pull_request: branches: - main defaults: run: shell: bash -l {0} jobs: # pre-job to read nox tests matrix - see https://stackoverflow.com/q/66747359/7262247 list_nox_test_sessions: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4.1.1 - name: Install python 3.9 uses: actions/setup-python@v5.0.0 with: python-version: 3.9 architecture: x64 - name: Install noxfile requirements run: pip install -r noxfile-requirements.txt - name: List 'tests' nox sessions and required python versions id: set-matrix run: echo "::set-output name=matrix::$(nox --json -l -s tests -v)" outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} # save nox sessions list to outputs run_all_tests: needs: list_nox_test_sessions strategy: fail-fast: false matrix: # see https://github.com/actions/setup-python/issues/544 # os: [ ubuntu-20.04 ] os: [ ubuntu-latest ] # , macos-latest, windows-latest] # all nox sessions: manually > dynamically from previous job # nox_session: ["tests-2.7", "tests-3.7"] nox_session: ${{ fromJson(needs.list_nox_test_sessions.outputs.matrix) }} name: ${{ matrix.os }} ${{ matrix.nox_session.python }} ${{ matrix.nox_session.session }} # ${{ matrix.name_suffix }} runs-on: ${{ matrix.os }} steps: - name: Checkout uses: actions/checkout@v4.1.1 - name: Install python ${{ matrix.nox_session.python }} for tests if: ${{ ! contains(fromJson('["3.13"]'), matrix.nox_session.python ) }} uses: MatteoH2O1999/setup-python@v3.2.1 # actions/setup-python@v5.0.0 id: set-py with: python-version: ${{ matrix.nox_session.python }} architecture: x64 allow-build: info cache-build: true - name: Install python ${{ matrix.nox_session.python }} for tests (3.13) if: contains(fromJson('["3.13"]'), matrix.nox_session.python ) uses: actions/setup-python@v5 id: set-py-latest with: # Include all versions including pre releases # See https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md#specifying-a-python-version python-version: ${{ format('~{0}.0-alpha.0', matrix.nox_session.python) }} architecture: x64 allow-build: info cache-build: true - name: Install python 3.12 for nox uses: actions/setup-python@v5.0.0 with: python-version: 3.12 architecture: x64 - name: pin virtualenv==20.15.1 in old python versions # pinned to keep compatibility with old versions, see https://github.com/MatteoH2O1999/setup-python/issues/28#issuecomment-1745613621 if: contains(fromJson('["2.7", "3.5", "3.6"]'), matrix.nox_session.python ) run: sed -i "s/virtualenv/virtualenv==20.15.1/g" noxfile-requirements.txt - name: Install noxfile requirements run: pip install -r noxfile-requirements.txt - name: Run nox session ${{ matrix.nox_session.session }} run: nox -s "${{ matrix.nox_session.session }}" -v # Share ./docs/reports so that they can be deployed with doc in next job - name: Share reports with other jobs # if: matrix.nox_session == '...': not needed, if empty won't be shared uses: actions/upload-artifact@v4.3.0 with: name: reports_dir path: ./docs/reports build_doc: runs-on: ubuntu-latest if: github.event_name == 'pull_request' steps: - name: Checkout uses: actions/checkout@v4.1.1 - name: Install python 3.9 for nox uses: actions/setup-python@v5.0.0 with: python-version: 3.9 architecture: x64 - name: Install noxfile requirements run: pip install -r noxfile-requirements.txt - name: Build the doc including example gallery run: nox -s docs -- build publish_release: needs: run_all_tests runs-on: ubuntu-latest if: github.event_name == 'push' steps: - name: GitHub context to debug conditional steps env: GITHUB_CONTEXT: ${{ toJSON(github) }} run: echo "$GITHUB_CONTEXT" - name: Checkout with no depth uses: actions/checkout@v4.1.1 with: fetch-depth: 0 # so that gh-deploy works - name: Install python 3.9 for nox uses: actions/setup-python@v5.0.0 with: python-version: 3.9 architecture: x64 # 1) retrieve the reports generated previously - name: Retrieve reports uses: actions/download-artifact@v4.1.1 with: name: reports_dir path: ./docs/reports # Nox install - name: Install noxfile requirements run: pip install -r noxfile-requirements.txt # 5) Run the flake8 report and badge - name: Run flake8 analysis and generate corresponding badge run: nox -s flake8 # -------------- only on Ubuntu + MAIN PUSH (no pull request, no tag) ----------- # 5) Publish the doc and test reports - name: \[not on TAG\] Publish documentation, tests and coverage reports if: github.event_name == 'push' && startsWith(github.ref, 'refs/heads') # startsWith(matrix.os,'ubuntu') run: nox -s publish # 6) Publish coverage report - name: \[not on TAG\] Create codecov.yaml with correct paths if: github.event_name == 'push' && startsWith(github.ref, 'refs/heads') shell: bash run: | cat << EOF > codecov.yml # codecov.yml fixes: - "/home/runner/work/smarie/python-makefun/::" # Correct paths EOF - name: \[not on TAG\] Publish coverage report if: github.event_name == 'push' && startsWith(github.ref, 'refs/heads') uses: codecov/codecov-action@v4.0.1 with: files: ./docs/reports/coverage/coverage.xml # -------------- only on Ubuntu + TAG PUSH (no pull request) ----------- # 7) Create github release and build the wheel - name: \[TAG only\] Build wheel and create github release if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') run: nox -s release -- ${{ secrets.GITHUB_TOKEN }} # 8) Publish the wheel on PyPi - name: \[TAG only\] Deploy on PyPi if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@v1.8.11 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} delete-artifacts: needs: publish_release runs-on: ubuntu-latest if: github.event_name == 'push' steps: - uses: kolpav/purge-artifacts-action@v1 with: token: ${{ secrets.GITHUB_TOKEN }} expire-in: 0 # Setting this to 0 will delete all artifacts python-makefun-1.15.4/.github/workflows/updater.yml000066400000000000000000000011771464421506400223470ustar00rootroot00000000000000name: GitHub Actions Version Updater # Controls when the action will run. on: workflow_dispatch: schedule: # Automatically run on every first day of the month - cron: '0 0 1 * *' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.1.1 with: # [Required] Access token with `workflow` scope. token: ${{ secrets.WORKFLOW_SECRET }} - name: Run GitHub Actions Version Updater uses: saadmk11/github-actions-version-updater@v0.8.1 with: # [Required] Access token with `workflow` scope. token: ${{ secrets.WORKFLOW_SECRET }} python-makefun-1.15.4/.gitignore000066400000000000000000000036301464421506400165470ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ src/makefun/_version.py # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv*/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # PyCharm development /.idea # OSX .DS_Store # JUnit and coverage reports docs/reports # ODSClient cache .odsclient python-makefun-1.15.4/LICENSE000066400000000000000000000030161464421506400155620ustar00rootroot00000000000000BSD 3-Clause License Copyright (c) 2019-2022, Sylvain MariƩ, Schneider Electric Industries All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. python-makefun-1.15.4/README.md000066400000000000000000000141561464421506400160430ustar00rootroot00000000000000# python-makefun *Small library to dynamically create python functions.* [![Python versions](https://img.shields.io/pypi/pyversions/makefun.svg)](https://pypi.python.org/pypi/makefun/) [![Build Status](https://github.com/smarie/python-makefun/actions/workflows/base.yml/badge.svg)](https://github.com/smarie/python-makefun/actions/workflows/base.yml) [![Tests Status](https://smarie.github.io/python-makefun/reports/junit/junit-badge.svg?dummy=8484744)](https://smarie.github.io/python-makefun/reports/junit/report.html) [![Coverage Status](https://smarie.github.io/python-makefun/reports/coverage/coverage-badge.svg?dummy=8484744)](https://smarie.github.io/python-makefun/reports/coverage/index.html) [![codecov](https://codecov.io/gh/smarie/python-makefun/branch/main/graph/badge.svg)](https://codecov.io/gh/smarie/python-makefun) [![Flake8 Status](https://smarie.github.io/python-makefun/reports/flake8/flake8-badge.svg?dummy=8484744)](https://smarie.github.io/python-makefun/reports/flake8/index.html) [![Documentation](https://img.shields.io/badge/doc-latest-blue.svg)](https://smarie.github.io/python-makefun/) [![PyPI](https://img.shields.io/pypi/v/makefun.svg)](https://pypi.python.org/pypi/makefun/) [![Downloads](https://pepy.tech/badge/makefun)](https://pepy.tech/project/makefun) [![Downloads per week](https://pepy.tech/badge/makefun/week)](https://pepy.tech/project/makefun) [![GitHub stars](https://img.shields.io/github/stars/smarie/python-makefun.svg)](https://github.com/smarie/python-makefun/stargazers) **This is the readme for developers.** The documentation for users is available here: [https://smarie.github.io/python-makefun/](https://smarie.github.io/python-makefun/) ## Want to contribute ? Contributions are welcome ! Simply fork this project on github, commit your contributions, and create pull requests. Here is a non-exhaustive list of interesting open topics: [https://github.com/smarie/python-makefun/issues](https://github.com/smarie/python-makefun/issues) ## `nox` setup This project uses `nox` to define all lifecycle tasks. In order to be able to run those tasks, you should create python 3.7 environment and install the requirements: ```bash >>> conda create -n noxenv python="3.7" >>> activate noxenv (noxenv) >>> pip install -r noxfile-requirements.txt ``` You should then be able to list all available tasks using: ``` >>> nox --list Sessions defined in \noxfile.py: * tests-2.7 -> Run the test suite, including test reports generation and coverage reports. * tests-3.5 -> Run the test suite, including test reports generation and coverage reports. * tests-3.6 -> Run the test suite, including test reports generation and coverage reports. * tests-3.8 -> Run the test suite, including test reports generation and coverage reports. * tests-3.7 -> Run the test suite, including test reports generation and coverage reports. - docs-3.7 -> Generates the doc and serves it on a local http server. Pass '-- build' to build statically instead. - publish-3.7 -> Deploy the docs+reports on github pages. Note: this rebuilds the docs - release-3.7 -> Create a release on github corresponding to the latest tag ``` ## Running the tests and generating the reports This project uses `pytest` so running `pytest` at the root folder will execute all tests on current environment. However it is a bit cumbersome to manage all requirements by hand ; it is easier to use `nox` to run `pytest` on all supported python environments with the correct package requirements: ```bash nox ``` Tests and coverage reports are automatically generated under `./docs/reports` for one of the sessions (`tests-3.7`). If you wish to execute tests on a specific environment, use explicit session names, e.g. `nox -s tests-3.6`. ## Editing the documentation This project uses `mkdocs` to generate its documentation page. Therefore building a local copy of the doc page may be done using `mkdocs build -f docs/mkdocs.yml`. However once again things are easier with `nox`. You can easily build and serve locally a version of the documentation site using: ```bash >>> nox -s docs nox > Running session docs-3.7 nox > Creating conda env in .nox\docs-3-7 with python=3.7 nox > [docs] Installing requirements with pip: ['mkdocs-material', 'mkdocs', 'pymdown-extensions', 'pygments'] nox > python -m pip install mkdocs-material mkdocs pymdown-extensions pygments nox > mkdocs serve -f ./docs/mkdocs.yml INFO - Building documentation... INFO - Cleaning site directory INFO - The following pages exist in the docs directory, but are not included in the "nav" configuration: - long_description.md INFO - Documentation built in 1.07 seconds INFO - Serving on http://127.0.0.1:8000 INFO - Start watching changes ... ``` While this is running, you can edit the files under `./docs/` and browse the automatically refreshed documentation at the local [http://127.0.0.1:8000](http://127.0.0.1:8000) page. Once you are done, simply hit `` to stop the session. Publishing the documentation (including tests and coverage reports) is done automatically by [the continuous integration engine](https://github.com/smarie/python-makefun/actions), using the `nox -s publish` session, this is not needed for local development. ## Packaging This project uses `setuptools_scm` to synchronise the version number. Therefore the following command should be used for development snapshots as well as official releases: `python setup.py sdist bdist_wheel`. However this is not generally needed since [the continuous integration engine](https://github.com/smarie/python-makefun/actions) does it automatically for us on git tags. For reference, this is done in the `nox -s release` session. ### Merging pull requests with edits - memo Ax explained in github ('get commandline instructions'): ```bash git checkout -b - main git pull https://github.com//python-makefun.git --no-commit --ff-only ``` if the second step does not work, do a normal auto-merge (do not use **rebase**!): ```bash git pull https://github.com//python-makefun.git --no-commit ``` Finally review the changes, possibly perform some modifications, and commit. python-makefun-1.15.4/ci_tools/000077500000000000000000000000001464421506400163705ustar00rootroot00000000000000python-makefun-1.15.4/ci_tools/.pylintrc000066400000000000000000000420031464421506400202340ustar00rootroot00000000000000[MASTER] # Specify a configuration file. #rcfile= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). # init-hook="import makefun" # Add files or directories to the blacklist. They should be base names, not # paths. ignore= # Pickle collected data for later comparisons. persistent=no # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= # Use multiple processes to speed up Pylint. # DO NOT CHANGE THIS VALUES >1 HIDE RESULTS!!!!! jobs=1 # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code extension-pkg-whitelist= # Allow optimization of some AST trees. This will activate a peephole AST # optimizer, which will apply various small optimizations. For instance, it can # be used to obtain the result of joining multiple strings with the addition # operator. Joining a lot of strings can lead to a maximum recursion error in # Pylint and this flag can prevent that. It has one side effect, the resulting # AST will be different than the one from reality. optimize-ast=no [MESSAGES CONTROL] # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED confidence= # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time. See also the "--disable" option for examples. disable=all enable=import-error, import-self, reimported, wildcard-import, misplaced-future, relative-import, deprecated-module, unpacking-non-sequence, invalid-all-object, undefined-all-variable, used-before-assignment, cell-var-from-loop, global-variable-undefined, redefined-builtin, redefine-in-handler, unused-import, unused-wildcard-import, global-variable-not-assigned, undefined-loop-variable, global-statement, global-at-module-level, bad-open-mode, redundant-unittest-assert, boolean-datetime, # Has common issues with our style due to # https://github.com/PyCQA/pylint/issues/210 unused-variable # Things we'd like to enable someday: # redefined-outer-name (requires a bunch of work to clean up our code first) # undefined-variable (re-enable when pylint fixes https://github.com/PyCQA/pylint/issues/760) # no-name-in-module (giving us spurious warnings https://github.com/PyCQA/pylint/issues/73) # unused-argument (need to clean up or code a lot, e.g. prefix unused_?) # Things we'd like to try. # Procedure: # 1. Enable a bunch. # 2. See if there's spurious ones; if so disable. # 3. Record above. # 4. Remove from this list. # deprecated-method, # anomalous-unicode-escape-in-string, # anomalous-backslash-in-string, # not-in-loop, # function-redefined, # continue-in-finally, # abstract-class-instantiated, # star-needs-assignment-target, # duplicate-argument-name, # return-in-init, # too-many-star-expressions, # nonlocal-and-global, # return-outside-function, # return-arg-in-generator, # invalid-star-assignment-target, # bad-reversed-sequence, # nonexistent-operator, # yield-outside-function, # init-is-generator, # nonlocal-without-binding, # lost-exception, # assert-on-tuple, # dangerous-default-value, # duplicate-key, # useless-else-on-loop, # expression-not-assigned, # confusing-with-statement, # unnecessary-lambda, # pointless-statement, # pointless-string-statement, # unnecessary-pass, # unreachable, # eval-used, # exec-used, # bad-builtin, # using-constant-test, # deprecated-lambda, # bad-super-call, # missing-super-argument, # slots-on-old-class, # super-on-old-class, # property-on-old-class, # not-an-iterable, # not-a-mapping, # format-needs-mapping, # truncated-format-string, # missing-format-string-key, # mixed-format-string, # too-few-format-args, # bad-str-strip-call, # too-many-format-args, # bad-format-character, # format-combined-specification, # bad-format-string-key, # bad-format-string, # missing-format-attribute, # missing-format-argument-key, # unused-format-string-argument, # unused-format-string-key, # invalid-format-index, # bad-indentation, # mixed-indentation, # unnecessary-semicolon, # lowercase-l-suffix, # fixme, # invalid-encoded-data, # unpacking-in-except, # import-star-module-level, # parameter-unpacking, # long-suffix, # old-octal-literal, # old-ne-operator, # backtick, # old-raise-syntax, # print-statement, # metaclass-assignment, # next-method-called, # dict-iter-method, # dict-view-method, # indexing-exception, # raising-string, # standarderror-builtin, # using-cmp-argument, # cmp-method, # coerce-method, # delslice-method, # getslice-method, # hex-method, # nonzero-method, # oct-method, # setslice-method, # apply-builtin, # basestring-builtin, # buffer-builtin, # cmp-builtin, # coerce-builtin, # old-division, # execfile-builtin, # file-builtin, # filter-builtin-not-iterating, # no-absolute-import, # input-builtin, # intern-builtin, # long-builtin, # map-builtin-not-iterating, # range-builtin-not-iterating, # raw_input-builtin, # reduce-builtin, # reload-builtin, # round-builtin, # unichr-builtin, # unicode-builtin, # xrange-builtin, # zip-builtin-not-iterating, # logging-format-truncated, # logging-too-few-args, # logging-too-many-args, # logging-unsupported-format, # logging-not-lazy, # logging-format-interpolation, # invalid-unary-operand-type, # unsupported-binary-operation, # no-member, # not-callable, # redundant-keyword-arg, # assignment-from-no-return, # assignment-from-none, # not-context-manager, # repeated-keyword, # missing-kwoa, # no-value-for-parameter, # invalid-sequence-index, # invalid-slice-index, # too-many-function-args, # unexpected-keyword-arg, # unsupported-membership-test, # unsubscriptable-object, # access-member-before-definition, # method-hidden, # assigning-non-slot, # duplicate-bases, # inconsistent-mro, # inherit-non-class, # invalid-slots, # invalid-slots-object, # no-method-argument, # no-self-argument, # unexpected-special-method-signature, # non-iterator-returned, # protected-access, # arguments-differ, # attribute-defined-outside-init, # no-init, # abstract-method, # signature-differs, # bad-staticmethod-argument, # non-parent-init-called, # super-init-not-called, # bad-except-order, # catching-non-exception, # bad-exception-context, # notimplemented-raised, # raising-bad-type, # raising-non-exception, # misplaced-bare-raise, # duplicate-except, # broad-except, # nonstandard-exception, # binary-op-exception, # bare-except, # not-async-context-manager, # yield-inside-async-function, # ... [REPORTS] # Set the output format. Available formats are text, parseable, colorized, msvs # (visual studio) and html. You can also give a reporter class, eg # mypackage.mymodule.MyReporterClass. output-format=parseable # Put messages in a separate file for each module / package specified on the # command line instead of printing them on stdout. Reports (if any) will be # written in a file name "pylint_global.[txt|html]". files-output=no # Tells whether to display a full report or only the messages reports=no # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (RP0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details #msg-template= [LOGGING] # Logging modules to check that the string format arguments are in logging # function parameter format logging-modules=logging [FORMAT] # Maximum number of characters on a single line. max-line-length=100 # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt=no # List of optional constructs for which whitespace checking is disabled. `dict- # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. # `trailing-comma` allows a space between comma and closing bracket: (a, ). # `empty-line` allows space-only lines. no-space-check=trailing-comma,dict-separator # Maximum number of lines in a module max-module-lines=1000 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # Number of spaces of indent required inside a hanging or continued line. indent-after-paren=4 # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. expected-line-ending-format= [TYPECHECK] # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis. It # supports qualified module names, as well as Unix pattern matching. ignored-modules= # List of classes names for which member attributes should not be checked # (useful for classes with attributes dynamically set). This supports can work # with qualified names. ignored-classes= # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. generated-members= [VARIABLES] # Tells whether we should check for unused import in __init__ files. init-import=no # A regular expression matching the name of dummy variables (i.e. expectedly # not used). dummy-variables-rgx=^_|^dummy # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins= # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. callbacks=cb_,_cb [SIMILARITIES] # Minimum lines number of a similarity. min-similarity-lines=4 # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes # Ignore imports when computing similarities. ignore-imports=no [SPELLING] # Spelling dictionary name. Available dictionaries: none. To make it working # install python-enchant package. spelling-dict= # List of comma separated words that should not be checked. spelling-ignore-words= # A path to a file that contains private dictionary; one word per line. spelling-private-dict-file= # Tells whether to store unknown words to indicated private dictionary in # --spelling-private-dict-file option instead of raising a message. spelling-store-unknown-words=no [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME,XXX,TODO [BASIC] # List of builtins function names that should not be used, separated by a comma bad-functions=map,filter,input # Good variable names which should always be accepted, separated by a comma good-names=i,j,k,ex,Run,_ # Bad variable names which should always be refused, separated by a comma bad-names=foo,bar,baz,toto,tutu,tata # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. name-group= # Include a hint for the correct naming format with invalid-name include-naming-hint=no # Regular expression matching correct function names function-rgx=[a-z_][a-z0-9_]{2,30}$ # Naming hint for function names function-name-hint=[a-z_][a-z0-9_]{2,30}$ # Regular expression matching correct variable names variable-rgx=[a-z_][a-z0-9_]{2,30}$ # Naming hint for variable names variable-name-hint=[a-z_][a-z0-9_]{2,30}$ # Regular expression matching correct constant names const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ # Naming hint for constant names const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ # Regular expression matching correct attribute names attr-rgx=[a-z_][a-z0-9_]{2,30}$ # Naming hint for attribute names attr-name-hint=[a-z_][a-z0-9_]{2,30}$ # Regular expression matching correct argument names argument-rgx=[a-z_][a-z0-9_]{2,30}$ # Naming hint for argument names argument-name-hint=[a-z_][a-z0-9_]{2,30}$ # Regular expression matching correct class attribute names class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ # Naming hint for class attribute names class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ # Regular expression matching correct inline iteration names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ # Naming hint for inline iteration names inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ # Regular expression matching correct class names class-rgx=[A-Z_][a-zA-Z0-9]+$ # Naming hint for class names class-name-hint=[A-Z_][a-zA-Z0-9]+$ # Regular expression matching correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Naming hint for module names module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Regular expression matching correct method names method-rgx=[a-z_][a-z0-9_]{2,30}$ # Naming hint for method names method-name-hint=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=^_ # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. docstring-min-length=-1 [ELIF] # Maximum number of nested blocks for function / method body max-nested-blocks=5 [IMPORTS] # Deprecated modules which should not be used, separated by a comma deprecated-modules=regsub,TERMIOS,Bastion,rexec # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled) import-graph= # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled) ext-import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled) int-import-graph= [DESIGN] # Maximum number of arguments for function / method max-args=5 # Argument names that match this expression will be ignored. Default to name # with leading underscore ignored-argument-names=_.* # Maximum number of locals for function / method body max-locals=15 # Maximum number of return / yield for function / method body max-returns=6 # Maximum number of branch for function / method body max-branches=12 # Maximum number of statements in function / method body max-statements=50 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of attributes for a class (see R0902). max-attributes=7 # Minimum number of public methods for a class (see R0903). min-public-methods=2 # Maximum number of public methods for a class (see R0904). max-public-methods=20 # Maximum number of boolean expressions in a if statement max-bool-expr=5 [CLASSES] # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=mcs # List of member names, which should be excluded from the protected access # warning. exclude-protected=_asdict,_fields,_replace,_source,_make [EXCEPTIONS] # Exceptions that will emit a warning when being caught. Defaults to # "Exception" overgeneral-exceptions=Exception python-makefun-1.15.4/ci_tools/check_python_version.py000066400000000000000000000023721464421506400231710ustar00rootroot00000000000000import sys if __name__ == "__main__": # Execute only if run as a script. # Check the arguments nbargs = len(sys.argv[1:]) if nbargs != 1: raise ValueError("a mandatory argument is required: ") expected_version_str = sys.argv[1] try: expected_version = tuple(int(i) for i in expected_version_str.split(".")) except Exception as e: raise ValueError("Error while parsing expected version %r: %r" % (expected_version, e)) if len(expected_version) < 1: raise ValueError("At least a major is expected") if sys.version_info[0] != expected_version[0]: raise AssertionError("Major version does not match. Expected %r - Actual %r" % (expected_version_str, sys.version)) if len(expected_version) >= 2 and sys.version_info[1] != expected_version[1]: raise AssertionError("Minor version does not match. Expected %r - Actual %r" % (expected_version_str, sys.version)) if len(expected_version) >= 3 and sys.version_info[2] != expected_version[2]: raise AssertionError("Patch version does not match. Expected %r - Actual %r" % (expected_version_str, sys.version)) print("SUCCESS - Actual python version %r matches expected one %r" % (sys.version, expected_version_str)) python-makefun-1.15.4/ci_tools/flake8-requirements.txt000066400000000000000000000007501464421506400230260ustar00rootroot00000000000000setuptools_scm>=3,<4 flake8>=3.6,<4 flake8-html>=0.4,<1 flake8-bandit>=2.1.1,<3 bandit<1.7.3 # temporary until this is fixed https://github.com/tylerwince/flake8-bandit/issues/21 flake8-bugbear>=20.1.0,<21.0.0 flake8-docstrings>=1.5,<2 flake8-print>=3.1.1,<4 flake8-tidy-imports>=4.2.1,<5 flake8-copyright==0.2.2 # Internal forked repo to fix an issue, keep specific version pydocstyle>=5.1.1,<6 pycodestyle>=2.6.0,<3 mccabe>=0.6.1,<1 naming>=0.5.1,<1 pyflakes>=2.2,<3 genbadge[flake8] python-makefun-1.15.4/ci_tools/github_release.py000066400000000000000000000131601464421506400217250ustar00rootroot00000000000000# a clone of the ruby example https://gist.github.com/valeriomazzeo/5491aee76f758f7352e2e6611ce87ec1 import os from os import path import re import click from click import Path from github import Github, UnknownObjectException # from valid8 import validate not compliant with python 2.7 @click.command() @click.option('-u', '--user', help='GitHub username') @click.option('-p', '--pwd', help='GitHub password') @click.option('-s', '--secret', help='GitHub access token') @click.option('-r', '--repo-slug', help='Repo slug. i.e.: apple/swift') @click.option('-cf', '--changelog-file', help='Changelog file path') @click.option('-d', '--doc-url', help='Documentation url') @click.option('-df', '--data-file', help='Data file to upload', type=Path(exists=True, file_okay=True, dir_okay=False, resolve_path=True)) @click.argument('tag') def create_or_update_release(user, pwd, secret, repo_slug, changelog_file, doc_url, data_file, tag): """ Creates or updates (TODO) a github release corresponding to git tag . """ # 1- AUTHENTICATION if user is not None and secret is None: # using username and password # validate('user', user, instance_of=str) assert isinstance(user, str) # validate('pwd', pwd, instance_of=str) assert isinstance(pwd, str) g = Github(user, pwd) elif user is None and secret is not None: # or using an access token # validate('secret', secret, instance_of=str) assert isinstance(secret, str) g = Github(secret) else: raise ValueError("You should either provide username/password OR an access token") click.echo("Logged in as {user_name}".format(user_name=g.get_user())) # 2- CHANGELOG VALIDATION regex_pattern = "[\s\S]*[\n][#]+[\s]*(?P[\S ]*%s[\S ]*)[\n]+?(?P<body>[\s\S]*?)[\n]*?(\n#|$)" % re.escape(tag) changelog_section = re.compile(regex_pattern) if changelog_file is not None: # validate('changelog_file', changelog_file, custom=os.path.exists, # help_msg="changelog file should be a valid file path") assert os.path.exists(changelog_file), "changelog file should be a valid file path" with open(changelog_file) as f: contents = f.read() match = changelog_section.match(contents).groupdict() if match is None or len(match) != 2: raise ValueError("Unable to find changelog section matching regexp pattern in changelog file.") else: title = match['title'] message = match['body'] else: title = tag message = '' # append footer if doc url is provided message += "\n\nSee [documentation page](%s) for details." % doc_url # 3- REPOSITORY EXPLORATION # validate('repo_slug', repo_slug, instance_of=str, min_len=1, help_msg="repo_slug should be a non-empty string") assert isinstance(repo_slug, str) and len(repo_slug) > 0, "repo_slug should be a non-empty string" repo = g.get_repo(repo_slug) # -- Is there a tag with that name ? try: tag_ref = repo.get_git_ref("tags/" + tag) except UnknownObjectException: raise ValueError("No tag with name %s exists in repository %s" % (tag, repo.name)) # -- Is there already a release with that tag name ? click.echo("Checking if release %s already exists in repository %s" % (tag, repo.name)) try: release = repo.get_release(tag) if release is not None: raise ValueError("Release %s already exists in repository %s. Please set overwrite to True if you wish to " "update the release (Not yet supported)" % (tag, repo.name)) except UnknownObjectException: # Release does not exist: we can safely create it. click.echo("Creating release %s on repo: %s" % (tag, repo.name)) click.echo("Release title: '%s'" % title) click.echo("Release message:\n--\n%s\n--\n" % message) repo.create_git_release(tag=tag, name=title, message=message, draft=False, prerelease=False) # add the asset file if needed if data_file is not None: release = None while release is None: release = repo.get_release(tag) release.upload_asset(path=data_file, label=path.split(data_file)[1], content_type="application/gzip") # --- Memo --- # release.target_commitish # 'master' # release.tag_name # '0.5.0' # release.title # 'First public release' # release.body # markdown body # release.draft # False # release.prerelease # False # # # release.author # release.created_at # datetime.datetime(2018, 11, 9, 17, 49, 56) # release.published_at # datetime.datetime(2018, 11, 9, 20, 11, 10) # release.last_modified # None # # # release.id # 13928525 # release.etag # 'W/"dfab7a13086d1b44fe290d5d04125124"' # release.url # 'https://api.github.com/repos/smarie/python-makefun/releases/13928525' # release.html_url # 'https://github.com/smarie/python-makefun/releases/tag/0.5.0' # release.tarball_url # 'https://api.github.com/repos/smarie/python-makefun/tarball/0.5.0' # release.zipball_url # 'https://api.github.com/repos/smarie/python-makefun/zipball/0.5.0' # release.upload_url # 'https://uploads.github.com/repos/smarie/python-makefun/releases/13928525/assets{?name,label}' if __name__ == '__main__': create_or_update_release() ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/ci_tools/nox_utils.py���������������������������������������������������������0000664�0000000�0000000�00000016265�14644215064�0021000�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from collections import namedtuple import logging from pathlib import Path import shutil import os from typing import Sequence, Dict, Union import nox nox_logger = logging.getLogger("nox") PY27, PY35, PY36, PY37, PY38, PY39, PY310, PY311, PY312, PY313 = ("2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13") DONT_INSTALL = "dont_install" def install_reqs( session, # pre wired phases setup=False, install=False, tests=False, extras=(), # custom phase phase=None, phase_reqs=None, versions_dct=None ): """ A high-level helper to install requirements from the various project files - pyproject.toml "[build-system] requires" (if setup=True) - setup.cfg "[options] setup_requires" (if setup=True) - setup.cfg "[options] install_requires" (if install=True) - setup.cfg "[options] test_requires" (if tests=True) - setup.cfg "[options.extras_require] <...>" (if extras=(a tuple of extras)) Two additional mechanisms are provided in order to customize how packages are installed. Conda packages -------------- If the session runs on a conda environment, you can add a [tool.conda] section to your pyproject.toml. This section should contain a `conda_packages` entry containing the list of package names that should be installed using conda instead of pip. ``` [tool.conda] # Declare that the following packages should be installed with conda instead of pip # Note: this includes packages declared everywhere, here and in setup.cfg conda_packages = [ "setuptools", "wheel", "pip" ] ``` Version constraints ------------------- In addition to the version constraints in the pyproject.toml and setup.cfg, you can specify additional temporary constraints with the `versions_dct` argument , for example if you know that this executes on a specific python version that requires special care. For this, simply pass a dictionary of {'pkg_name': 'pkg_constraint'} for example {"pip": ">10"}. """ # Read requirements from pyproject.toml toml_setup_reqs, toml_use_conda_for = read_pyproject_toml() if setup: install_any(session, "pyproject.toml#build-system", toml_setup_reqs, use_conda_for=toml_use_conda_for, versions_dct=versions_dct) # Read test requirements from setup.cfg setup_cfg = read_setuptools_cfg() if setup: install_any(session, "setup.cfg#setup_requires", setup_cfg.setup_requires, use_conda_for=toml_use_conda_for, versions_dct=versions_dct) if install: install_any(session, "setup.cfg#install_requires", setup_cfg.install_requires, use_conda_for=toml_use_conda_for, versions_dct=versions_dct) if tests: install_any(session, "setup.cfg#tests_requires", setup_cfg.tests_requires, use_conda_for=toml_use_conda_for, versions_dct=versions_dct) for extra in extras: install_any(session, "setup.cfg#extras_require#%s" % extra, setup_cfg.extras_require[extra], use_conda_for=toml_use_conda_for, versions_dct=versions_dct) if phase is not None: install_any(session, phase, phase_reqs, use_conda_for=toml_use_conda_for, versions_dct=versions_dct) def install_any(session, phase_name: str, pkgs: Sequence[str], use_conda_for: Sequence[str] = (), versions_dct: Dict[str, str] = None, ): """Install the `pkgs` provided with `session.install(*pkgs)`, except for those present in `use_conda_for`""" # use the provided versions dictionary to update the versions if versions_dct is None: versions_dct = dict() pkgs = [pkg + versions_dct.get(pkg, "") for pkg in pkgs if versions_dct.get(pkg, "") != DONT_INSTALL] nox_logger.debug("\nAbout to install *%s* requirements: %s.\n " "Conda pkgs are %s" % (phase_name, pkgs, use_conda_for)) # install on conda... if the session uses conda backend if not isinstance(session.virtualenv, nox.virtualenv.CondaEnv): conda_pkgs = [] else: conda_pkgs = [pkg_req for pkg_req in pkgs if any(get_req_pkg_name(pkg_req) == c for c in use_conda_for)] if len(conda_pkgs) > 0: nox_logger.info("[%s] Installing requirements with conda: %s" % (phase_name, conda_pkgs)) session.conda_install(*conda_pkgs) pip_pkgs = [pkg_req for pkg_req in pkgs if pkg_req not in conda_pkgs] # safety: make sure that nothing went modified or forgotten assert set(conda_pkgs).union(set(pip_pkgs)) == set(pkgs) if len(pip_pkgs) > 0: nox_logger.info("[%s] Installing requirements with pip: %s" % (phase_name, pip_pkgs)) session.install(*pip_pkgs) # ------------- requirements related def read_pyproject_toml() -> Union[list, list]: """ Reads the `pyproject.toml` and returns - a list of setup requirements from [build-system] requires - sub-list of these requirements that should be installed with conda, from [tool.my_conda] conda_packages """ if os.path.exists("pyproject.toml"): import toml nox_logger.debug("\nA `pyproject.toml` file exists. Loading it.") pyproject = toml.load("pyproject.toml") requires = pyproject['build-system']['requires'] try: conda_pkgs = pyproject['tool']['conda']['conda_packages'] except KeyError: conda_pkgs = [] return requires, conda_pkgs else: raise FileNotFoundError("No `pyproject.toml` file exists. No dependency will be installed ...") SetupCfg = namedtuple('SetupCfg', ('setup_requires', 'install_requires', 'tests_requires', 'extras_require')) def read_setuptools_cfg(): """ Reads the `setup.cfg` file and extracts the various requirements lists """ # see https://stackoverflow.com/a/30679041/7262247 from setuptools import Distribution dist = Distribution() dist.parse_config_files() return SetupCfg(setup_requires=dist.setup_requires, install_requires=dist.install_requires, tests_requires=dist.tests_require, extras_require=dist.extras_require) def get_req_pkg_name(r): """Return the package name part of a python package requirement. For example "funcsigs;python<'3.5'" will return "funcsigs" "pytest>=3" will return "pytest" """ return r.replace('<', '=').replace('>', '=').replace(';', '=').split("=")[0] # ----------- other goodies def rm_file(folder: Union[str, Path]): """Since on windows Path.unlink throws permission error sometimes, os.remove is preferred.""" if isinstance(folder, str): folder = Path(folder) if folder.exists(): os.remove(str(folder)) # Folders.site.unlink() --> possible PermissionError def rm_folder(folder: Union[str, Path]): """Since on windows Path.unlink throws permission error sometimes, shutil is preferred.""" if isinstance(folder, str): folder = Path(folder) if folder.exists(): shutil.rmtree(str(folder)) # Folders.site.unlink() --> possible PermissionError �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/docs/�������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14644215064�0015505�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/docs/api_reference.md���������������������������������������������������������0000664�0000000�0000000�00000045533�14644215064�0020630�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# API reference In general, using `help(symbol)` is the recommended way to get the latest documentation. In addition, this page provides an overview of the various elements in this package. ## Main symbols ### `create_function` ```python def create_function(func_signature: Union[str, Signature], func_impl: Callable[[Any], Any], func_name: str = None, inject_as_first_arg: bool = False, add_source: bool = True, add_impl: bool = True, doc: str = None, qualname: str = None, co_name: str = None, module_name: str = None, **attrs): ``` Creates a function with signature `func_signature` that will call `func_impl` when called. All arguments received by the generated function will be propagated as keyword-arguments to `func_impl` when it is possible (so all the time, except for var-positional or positional-only arguments that get passed as *args. Note that positional-only does not yet exist in python but this case is already covered because it is supported by `Signature` objects). `func_signature` can be provided in different formats: - as a string containing the name and signature without 'def' keyword, such as `'foo(a, b: int, *args, **kwargs)'`. In which case the name in the string will be used for the `__name__` and `__qualname__` of the created function by default. - as a `Signature` object, for example created using `signature(f)` or handcrafted. Since a `Signature` object does not contain any name, in this case the `__name__` and `__qualname__` of the created function will be copied from `func_impl` by default. All the other metadata of the created function are defined as follows: - default `__name__` attribute (see above) can be overridden by providing a non-None `func_name` - default `__qualname__` attribute (see above) can be overridden by providing a non-None `qualname` - `__annotations__` attribute is created to match the annotations in the signature. - `__doc__` attribute is copied from `func_impl.__doc__` except if overridden using `doc` - `__module__` attribute is copied from `func_impl.__module__` except if overridden using `module_name` - `__code__.co_name` (see above) defaults to the same value as the above `__name__` attribute, except when that value is not a valid Python identifier, in which case it will be `<lambda>`. It can be overridden by providing a `co_name` that is either a valid Python identifier or `<lambda>`. Finally two new attributes are optionally created - `__source__` attribute: set if `add_source` is `True` (default), this attribute contains the source code of the generated function - `__func_impl__` attribute: set if `add_impl` is `True` (default), this attribute contains a pointer to `func_impl` A lambda function will be created in the following cases: - when `func_signature` is a `Signature` object and `func_impl` is itself a lambda function, - when the function name, either derived from a `func_signature` string, or given explicitly with `func_name`, is not a valid Python identifier, or - when the provided `co_name` is `<lambda>`. **Parameters:** * `func_signature`: either a string without 'def' such as "foo(a, b: int, *args, **kwargs)" or "(a, b: int)", or a `Signature` object, for example from the output of `inspect.signature` or from the `funcsigs.signature` backport. Note that these objects can be created manually too. If the signature is provided as a string and contains a non-empty name, this name will be used instead of the one of the decorated function. * `func_impl`: the function that will be called when the generated function is executed. Its signature should be compliant with (=more generic than) `func_signature` * `inject_as_first_arg`: if `True`, the created function will be injected as the first positional argument of `func_impl`. This can be handy in case the implementation is shared between several facades and needs to know from which context it was called. Default=`False` * `func_name`: provide a non-`None` value to override the created function `__name__` and `__qualname__`. If this is `None` (default), the `__name__` will default to the one of `func_impl` if `func_signature` is a `Signature`, or to the name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name. * `add_source`: a boolean indicating if a '__source__' annotation should be added to the generated function (default: True) * `add_impl`: a boolean indicating if a '__func_impl__' annotation should be added to the generated function (default: True) * `doc`: a string representing the docstring that will be used to set the __doc__ attribute on the generated function. If None (default), the doc of func_impl will be used. * `qualname`: a string representing the qualified name to be used. If None (default), the `__qualname__` will default to the one of `func_impl` if `func_signature` is a `Signature`, or to the name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name. * `co_name`: a string representing the name to be used in the compiled code of the function. If None (default), the `__code__.co_name` will default to the one of `func_impl` if `func_signature` is a `Signature`, or to the name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name. * `module_name`: the name of the module to be set on the function (under __module__ ). If None (default), `func_impl.__module__` will be used. * `attrs`: other keyword attributes that should be set on the function. Note that `func_impl.__dict__` is not automatically copied. ### `@with_signature` ```python def with_signature(func_signature: Union[str, Signature], func_name: str = None, inject_as_first_arg: bool = False, add_source: bool = True, add_impl: bool = True, doc: str = None, qualname: str = None, co_name: str = None, module_name: str = None, **attrs ): ``` A decorator for functions, to change their signature. The new signature should be compliant with the old one. ```python @with_signature(<arguments>) def impl(...): ... ``` is totally equivalent to `impl = create_function(<arguments>, func_impl=impl)` except for one additional behaviour: - If `func_signature` is set to `None`, there is no `TypeError` as in create_function. Instead, this simply applies the new metadata (name, doc, module_name, attrs) to the decorated function without creating a wrapper. `add_source`, `add_impl` and `inject_as_first_arg` should **not** be set in this case. * `func_signature`: the new signature of the decorated function. Either a string without 'def' such as "foo(a, b: int, *args, **kwargs)" or "(a, b: int)", or a `Signature` object, for example from the output of `inspect.signature` or from the `funcsigs.signature` backport. Note that these objects can be created manually too. If the signature is provided as a string and contains a non-empty name, this name will be used instead of the one of the decorated function. Finally `None` can be provided to indicate that user wants to only change the medatadata (func_name, doc, module_name, attrs) of the decorated function, without generating a new function. * `inject_as_first_arg`: if `True`, the created function will be injected as the first positional argument of `func_impl`. This can be handy in case the implementation is shared between several facades and needs to know from which context it was called. Default=`False` * `func_name`: provide a non-`None` value to override the created function `__name__` and `__qualname__`. If this is `None` (default), the `__name__` will default to the one of `func_impl` if `func_signature` is a `Signature`, or to the name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name. * `add_source`: a boolean indicating if a '__source__' annotation should be added to the generated function (default: True) * `add_impl`: a boolean indicating if a '__func_impl__' annotation should be added to the generated function (default: True) * `doc`: a string representing the docstring that will be used to set the __doc__ attribute on the generated function. If None (default), the doc of the decorated function will be used. * `qualname`: a string representing the qualified name to be used. If None (default), the `__qualname__` will default to the one of `func_impl` if `func_signature` is a `Signature`, or to the name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name. * `co_name`: a string representing the name to be used in the compiled code of the function. If None (default), the `__code__.co_name` will default to the one of `func_impl` if `func_signature` is a `Signature`, or to the name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name. * `module_name`: the name of the module to be set on the function (under __module__ ). If None (default), the `__module__` attribute of the decorated function will be used. * `attrs`: other keyword attributes that should be set on the function. Note that the full `__dict__` of the decorated function is not automatically copied. ### `@wraps` ```python def wraps(f, new_sig: Union[str, Signature] = None, prepend_args: Union[str, Parameter, Iterable[Union[str, Parameter]]] = None, append_args: Union[str, Parameter, Iterable[Union[str, Parameter]]] = None, remove_args: Union[str, Iterable[str]] = None, func_name: str = None, inject_as_first_arg: bool = False, add_source: bool = True, add_impl: bool = True, doc: str = None, qualname: str = None, co_name: str = None, module_name: str = None, **attrs ): ``` A decorator to create a signature-preserving wrapper function. It is similar to `functools.wraps`, but - relies on a proper dynamically-generated function. Therefore as opposed to `functools.wraps`, - the wrapper body will not be executed if the arguments provided are not compliant with the signature - instead a `TypeError` will be raised before entering the wrapper body. - the arguments will always be received as keywords by the wrapper, when possible. See [documentation](./index.md#signature-preserving-function-wrappers) for details. - **you can modify the signature** of the resulting function, by providing a new one with `new_sig` or by providing a list of arguments to remove in `remove_args`, to prepend in `prepend_args`, or to append in `append_args`. See documentation on [full](./index.md#editing-a-signature) and [quick](./index.md#easier-edits) signature edits for details. Comparison with `@with_signature`: `@wraps(f)` is equivalent to `@with_signature(signature(f), func_name=f.__name__, doc=f.__doc__, module_name=f.__module__, qualname=f.__qualname__, __wrapped__=f, **f.__dict__, **attrs)` In other words, as opposed to `@with_signature`, the metadata (doc, module name, etc.) is provided by the wrapped `wrapped_fun`, so that the created function seems to be identical (except possiblyfor the signature). Note that all options in `with_signature` can still be overrided using parameters of `@wraps`. The additional `__wrapped__` attribute is added on the created function, to stay consistent with the `functools.wraps` behaviour. If the signature is modified through `new_sig`, `remove_args`, `append_args` or `prepend_args`, the `__signature__` attribute will be added per [PEP 362](https://peps.python.org/pep-0362/). See also [python documentation on @wraps](https://docs.python.org/3/library/functools.html#functools.wraps) **Parameters** - `wrapped_fun`: the function that you intend to wrap with the decorated function. As in `functools.wraps`, `wrapped_fun` is used as the default reference for the exposed signature, `__name__`, `__qualname__`, `__doc__` and `__dict__`. - `new_sig`: the new signature of the decorated function. By default it is `None` and means "same signature as in `wrapped_fun`" (similar behaviour as in `functools.wraps`) If you wish to modify the exposed signature you can either use `remove/prepend/append_args`, or pass a non-None `new_sig`. It can be either a string without 'def' such as "foo(a, b: int, *args, **kwargs)" of "(a, b: int)", or a `Signature` object, for example from the output of `inspect.signature` or from the `funcsigs.signature` backport. Note that these objects can be created manually too. If the signature is provided as a string and contains a non-empty name, this name will be used instead of the one of `wrapped_fun`. - `prepend_args`: a string or list of strings to prepend to the signature of `wrapped_fun`. These extra arguments should not be passed to `wrapped_fun`, as it does not know them. This is typically used to easily create a wrapper with additional arguments, without having to manipulate the signature objects. - `append_args`: a string or list of strings to append to the signature of `wrapped_fun`. These extra arguments should not be passed to `wrapped_fun`, as it does not know them. This is typically used to easily create a wrapper with additional arguments, without having to manipulate the signature objects. - `remove_args`: a string or list of strings to remove from the signature of `wrapped_fun`. These arguments should be injected in the received `kwargs` before calling `wrapped_fun`, as it requires them. This is typically used to easily create a wrapper with less arguments, without having to manipulate the signature objects. - `func_name`: provide a non-`None` value to override the created function `__name__` and `__qualname__`. If this is `None` (default), the `__name__` will default to the ones of `wrapped_fun` if `new_sig` is `None` or is a `Signature`, or to the name defined in `new_sig` if `new_sig` is a `str` and contains a non-empty name. - `inject_as_first_arg`: if `True`, the created function will be injected as the first positional argument of the decorated function. This can be handy in case the implementation is shared between several facades and needs to know from which context it was called. Default=`False` - `add_source`: a boolean indicating if a '__source__' annotation should be added to the generated function (default: True) - `add_impl`: a boolean indicating if a '__func_impl__' annotation should be added to the generated function (default: True) - `doc`: a string representing the docstring that will be used to set the __doc__ attribute on the generated function. If None (default), the doc of `wrapped_fun` will be used. If `wrapped_fun` is an instance of `functools.partial`, a special enhanced doc will be generated. - `qualname`: a string representing the qualified name to be used. If None (default), the `__qualname__` will default to the one of `wrapped_fun`, or the one in `new_sig` if `new_sig` is provided as a string with a non-empty function name. - `co_name`: a string representing the name to be used in the compiled code of the function. If None (default), the `__code__.co_name` will default to the one of `func_impl` if `func_signature` is a `Signature`, or to the name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name. - `module_name`: the name of the module to be set on the function (under __module__ ). If None (default), the `__module__` attribute of `wrapped_fun` will be used. - `attrs`: other keyword attributes that should be set on the function. Note that the full `__dict__` of `wrapped_fun` is automatically copied. ### `create_wrapper` ```python def create_wrapper(wrapped, wrapper, new_sig: Union[str, Signature] = None, prepend_args: Union[str, Parameter, Iterable[Union[str, Parameter]]] = None, append_args: Union[str, Parameter, Iterable[Union[str, Parameter]]] = None, remove_args: Union[str, Iterable[str]] = None, func_name: str = None, inject_as_first_arg: bool = False, add_source: bool = True, add_impl: bool = True, doc: str = None, qualname: str = None, co_name: str = None, module_name: str = None, **attrs ): ``` Creates a signature-preserving wrapper function. `create_wrapper(wrapped, wrapper, **kwargs)` is equivalent to `wraps(wrapped, **kwargs)(wrapper)`. See [`@wraps`](#wraps) ### `@partial` ```python def partial(f: Callable, *preset_pos_args, **preset_kwargs ): ``` Equivalent of `functools.partial` but relies on a dynamically-created function. As a result the function looks nicer to users in terms of apparent documentation, name, etc. See [documentation](./index.md#removing-parameters-easily) for details. ### `@with_partial` ```python def with_partial(*preset_pos_args, **preset_kwargs ): ``` Decorator to 'partialize' a function using [`partial`](#partial). ## Signature editing utils ### `add_signature_parameters` ```python def add_signature_parameters(s, # type: Signature first=(), # type: Union[str, Parameter, Iterable[Union[str, Parameter]]] last=(), # type: Union[str, Parameter, Iterable[Union[str, Parameter]]] custom=(), # type: Union[Parameter, Iterable[Parameter]] custom_idx=-1 # type: int ): ``` Adds the provided parameters to the signature `s` (returns a new `Signature` instance). - `s`: the original signature to edit - `first`: a single element or a list of `Parameter` instances to be added at the beginning of the parameter's list. Strings can also be provided, in which case the parameter kind will be created based on best guess. - `last`: a single element or a list of `Parameter` instances to be added at the end of the parameter's list. Strings can also be provided, in which case the parameter kind will be created based on best guess. - `custom`: a single element or a list of `Parameter` instances to be added at a custom position in the list. That position is determined with `custom_idx` - `custom_idx`: the custom position to insert the `custom` parameters to. ### `remove_signature_parameters` ```python def remove_signature_parameters(s, *param_names): ``` Removes the provided parameters from the signature `s` (returns a new `Signature` instance). ## Pseudo-compilation ### `compile_fun` ���������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/docs/changelog.md�������������������������������������������������������������0000664�0000000�0000000�00000043026�14644215064�0017763�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Changelog ### 1.15.4 - Python 3.13 official support - Python 3.13 is now supported. PR [#108](https://github.com/smarie/python-makefun/pull/108) and PR [#104](https://github.com/smarie/python-makefun/pull/104) by [mgorny](https://github.com/mgorny) ### 1.15.3 - bugfix - Fixed `SyntaxError: invalid syntax` happening when the default value of one argument of the created function is a subclass of a basic primitive. Fixes [#98](https://github.com/smarie/python-makefun/issues/98). PR [#99](https://github.com/smarie/python-makefun/pull/99) by [moskupols](https://github.com/moskupols). ### 1.15.2 - bugfix - Fixed `SyntaxError` happening when the name of a native coroutine function to create contains `'return'`. Fixes [#96](https://github.com/smarie/python-makefun/issues/96). ### 1.15.1 - bugfixes - Fixed `ValueError: Invalid co_name` happening on python 2 when the name of a function to create starts or ends with `_` or contains a double `__` . Fixes [#91](https://github.com/smarie/python-makefun/issues/91) ### 1.15.0 - More PEP-compliant `wraps` - `wraps` now always sets the `__wrapped__` attribute, and also sets the `__signature__` attribute when the signature changes, as specified by PEP 362. PR [#86](https://github.com/smarie/python-makefun/pull/86) by [lucaswiman](https://github.com/lucaswiman). ### 1.14.0 - Support for lambda functions - `create_wrapper`, `create_function`, `wraps` and `with_signature` now support lambda functions. They also accept a new parameter `co_name` to define the name to be used in the compiled code. PR [#80](https://github.com/smarie/python-makefun/pull/80) by [andrewcleveland](https://github.com/andrewcleveland). ### 1.13.1 - Fixed regression with generators in python 3.5 - Fixed an issue where using `partial` on a generator function in python 3.5 was raising a `SyntaxError`. Fixed [#79](https://github.com/smarie/python-makefun/issues/79) ### 1.13.0 - Support for async generator functions - async generator functions are now supported (See [PEP525](https://www.python.org/dev/peps/pep-0525/)). Fixed [#77](https://github.com/smarie/python-makefun/issues/77). [PR#78](https://github.com/smarie/python-makefun/pull/78) by [broglep-work](https://github.com/broglep-work). ### 1.12.1 - Bugfix - Fixed `TypeError` when a `func` attribute is present on the function provided to `create_function`. Fixed [#76](https://github.com/smarie/python-makefun/issues/76) ### 1.12.0 - Refactoring and consistency improvement - Fixed `partial` so that : - when no argument is provided, it is equivalent to `wraps(f)(f)`. That is, the `__wrapped__` attribute is set. Fixed [#73](https://github.com/smarie/python-makefun/issues/73) - it sets the `func` attribute just as `functools.partial` does. Fixed [#75](https://github.com/smarie/python-makefun/issues/75) - Removed `pytest-cases` dependency as it was a circular one. Fixed [#68](https://github.com/smarie/python-makefun/issues/68) - Now using `flake8` for qualimetry and `genbadge` for badges. Fixed [#71](https://github.com/smarie/python-makefun/issues/71) - Restructured project so that tests are truly independent, to ease rpm/apt/etc. packaging. Fixed [#69](https://github.com/smarie/python-makefun/issues/69) ### 1.11.3 - bugfix with default values representable but not evaluable - When a default value `v` in a signature is representable but its `eval(repr(v))` raises an exception, created signatures would raise an exception instead of automatically protecting the symbol. PR [#67](https://github.com/smarie/python-makefun/pull/67) by [gcalmettes](https://github.com/gcalmettes). ### 1.11.2 - bugfix with chained `@wraps` - Fixed issue where `@wraps` does not remove the `__wrapped__` attribute when it wraps an already-wrapped function, and modifies its signature. Fixes [#66](https://github.com/smarie/python-makefun/issues/66) ### 1.11.1 - bugfix - Fixed a bug happening when trying to generate a function where `float('inf')` is used as a default value. Fixes [#63](https://github.com/smarie/python-makefun/issues/63). ### 1.11.0 - `partial` bugfixes and improvements + new `wraps` parameters - `@wraps` now has new arguments `prepend_arg` and `append_arg` to quickly prepend or append a new parameter to a function signature. Fixes [#65](https://github.com/smarie/python-makefun/issues/65) - `add_signature_parameters` can now receive string parameter names in its `first` and `last` arguments. - Improved generated docstring for `partial` with better support for preset positional args. - `partial`'s behaviour is now aligned with the one in `functools.partial` in terms of arguments order and kind. Fixes [#64](https://github.com/smarie/python-makefun/issues/64) - Bug fix: passing preset positional arguments to `partial` does not wipe out the rest of the signature any more. Fixes [#62](https://github.com/smarie/python-makefun/issues/62) ### 1.10.2 - Github actions test - Technical release: validation of the new github actions workflow. ### 1.10.1 - Github actions migration - Technical release: validation of the new github actions workflow for PyPi deployment. ### 1.10.0 - Removing arguments easily in `@wraps` - `@wraps` now offers a `remove_params` argument allowing one to remove an argument from the exposed signature, typically to create and inject it in the wrapper. Fixes [#60](https://github.com/smarie/python-makefun/issues/60) ### 1.9.5 - Bugfix with `partial` when f has no args - `partial` can now be used to create a copy of a function with no args. Fixed `ValueError: Cannot preset 0 positional args, function case_second has only 0 args.`. Fixed [#59](https://github.com/smarie/python-makefun/issues/59) ### 1.9.4 - removed six dependency - Removed unused `six` dependency. Note: this version will ship in conda forge. ### 1.9.3 - Minor compatibility fixes with functools.partial - `@wraps` can now be used to wrap a `functools.partial`. Fixed [#57](https://github.com/smarie/python-makefun/issues/57) - `create_function` now correctly reuses information from the partialized function if a `functools.partial` is used as the implementation. - `create_function` now raises a more explicit error when no `func_name` can be found. - `makefun.partial` now generates a signature that is more consistent with `functools.partial`. Fixes [#58](https://github.com/smarie/python-makefun/issues/58) ### 1.9.2 - packaging improvements - packaging improvements: set the "universal wheel" flag to 1, and cleaned up the `setup.py`. In particular removed dependency to `six` for setup and added `py.typed` file, as well as set the `zip_safe` flag to False. Removed tests folder from package. Fixes [#54](https://github.com/smarie/python-makefun/issues/54) ### 1.9.1 - `@compile_fun` bugfix Fixed `OSError: could not get source code` or `IOError: could not get source code` when `@compile_fun` is used on a function that depends on an already-compiled function. Fixed [#51](https://github.com/smarie/python-makefun/issues/51) ### 1.9.0 - `@compile_fun` improvements, bugfix and better exception `@compile_fun`: added capability to disable recursive compilation (`recurse` arg) , and to exclude some names from compilation (`except_names` arg). Fixed [#49](https://github.com/smarie/python-makefun/issues/49) and [#50](https://github.com/smarie/python-makefun/issues/50) Fixed issue `ValueError: Cell is empty` with `@compile_fun`. Fixed [#48](https://github.com/smarie/python-makefun/issues/48) Now raising an `UndefinedSymbolError` when a symbol is not known at compilation time. One step towards [#47](https://github.com/smarie/python-makefun/issues/47) ### 1.8.0 - new `@compile_fun` goodie New goodie `@compile_fun` decorator to `compile` a function so that it can not be navigated to using the debugger. Fixes [#46](https://github.com/smarie/python-makefun/issues/46) ### 1.7.0 - minor goodies update `add_signature_parameters` now accepts that one specifies a custom index where to insert the new parameters. ### 1.6.11 - Added __version__ attribute Added `__version__` attribute to comply with PEP396, following [this guide](https://smarie.github.io/python-getversion/#package-versioning-best-practices). Fixes [#45](https://github.com/smarie/python-makefun/issues/45). ### 1.6.10 - Fixed dependencies 2 Fixed `six` dependency: also declared as a setup dependency. ### 1.6.9 - Fixed dependencies Added missing `six` dependency explicitly. ### 1.6.8 - Improved performance * Improved performance of inner method `get_signature_string` (used by all entry points) after profiling. ### 1.6.7 - Increased tolerance to function signatures in python 2 * In python 2 some libraries such as `attrs` can modify the annotations manually, making `signature` return a string representation that is not compliant with the language version. This raised a `SyntaxError` in previous versions. The new version silently removes all these annotations in python versions that do not support them. Fixes [#39](https://github.com/smarie/python-makefun/issues/39). ### 1.6.6 - Bug fix * Fixed yet another nasty varpositional-related bug :). Fixes [#38](https://github.com/smarie/python-makefun/issues/38). ### 1.6.5 - Bug fix * Fixed `NameError` in case of unknown symbols in type hints. Fixes [#37](https://github.com/smarie/python-makefun/issues/37). ### 1.6.4 - Bug fix and minor improvement * Fixed PEP8 error in source code. Fixes [#35](https://github.com/smarie/python-makefun/issues/35). * Now string signatures can contain a colon. Fixes [#36](https://github.com/smarie/python-makefun/issues/36) ### 1.6.3 - Bug fix with type hints in signature Fixed bug when the return type annotation of the function to create contains non-locally available type hints. Fixes [#33](https://github.com/smarie/python-makefun/issues/33). ### 1.6.2 - Bug fix with type hints in signature Fixed bug when the signature of the function to create contains non-locally available type hints. Fixes [#32](https://github.com/smarie/python-makefun/issues/32). ### 1.6.1 - `with_partial` and `partial` minor bug fix Fixed `partial` to support missing and empty docstring. Fixes [#31](https://github.com/smarie/python-makefun/issues/31). ### 1.6.0 - added `with_partial` and `partial` New method `partial` that behaves like `functools.partial`, and equivalent decorator `@with_partial`. Fixes [#30](https://github.com/smarie/python-makefun/issues/30). ### 1.5.1 - bug fix `add_signature_parameters` now correctly inserts parameters in the right order when they are prepended (using `first=`). Fixed [#29](https://github.com/smarie/python-makefun/issues/29). ### 1.5.0 - Major refactoring and bugfixes **Function creation API:** - renamed all `handler` into `impl` for clarity. Fixes [#27](https://github.com/smarie/python-makefun/issues/27). - renamed `addsource` and `addhandler` arguments as `add_source` and `add_impl` respectively, for consistency - signatures can not be provided as a callable anymore - that was far too confusing. If the reference signature is a callable, then use `@wraps` or `create_wrapper`, because that's probably what you want to do (= reuse not only the signature but also all metadata). Fixes [#26](https://github.com/smarie/python-makefun/issues/26). - the function name is now optional in signatures provided as string. - now setting `__qualname__` attribute - default function name, qualname, doc and module name are the ones from `func_impl` in `create_function` and `@with_signature`, and are the ones from the wrapped function in `create_wrapper` and `@wraps` as intuitively expected. Fixes [#28](https://github.com/smarie/python-makefun/issues/28). **Wrappers:** - `@wraps` and `create_wrapper` now offer a `new_sig` argument. In that case the `__wrapped__` attribute is not set. Fixes [#25](https://github.com/smarie/python-makefun/issues/25). - `@wraps` and `create_wrapper` now correctly preserve the `__dict__` and other metadata from the wrapped item. Fixes [#24](https://github.com/smarie/python-makefun/issues/24) ### 1.4.0 - Non-representable default values are now handled correctly When a non-representable default value was used in the signature to generate, the code failed with a `SyntaxError`. This case is now correctly handled, by storing the corresponding variable in the generated function's context. Fixes [#23](https://github.com/smarie/python-makefun/issues/23). ### 1.3.0 - Aliases for signature-preserving wrapper scenarios - Now providing a `@wraps`, equivalent of `functools.wraps`; and a `create_wrapper` equivalent of `functools.update_wrapper`. Fixes [#21](https://github.com/smarie/python-makefun/issues/21) - `@with_signature` now does not override the `__name__` when signature is provided as a function. Fixes [#22](https://github.com/smarie/python-makefun/issues/22) - `add_signature_parameters` now accepts that parameters are provided as single elements (not necessarily iterables) - Updated documentation ### 1.2.0 - `@with_signature` supports `None` `None` can be used as the desired signature of `@with_signature`. This indicated that the user does not want to create a new function but only wants to update the metadata. Fixes [#20](https://github.com/smarie/python-makefun/issues/20). ### 1.1.2 - Fixes Fixed `isgeneratorfunction` for old python versions, see [decorator#63](https://github.com/micheles/decorator/pull/63). Python<3.3-specific function body is now not loaded at all if not needed. ### 1.1.1 - `@with_signature` fix `inject_as_first_arg` was missing from `@with_signature`, added it. Fixed [#18](https://github.com/smarie/python-makefun/issues/18). ### 1.1.0 - Support for generators and coroutines Now `create_function` and `@with_signature` create the same kind of function than the handler. So if it is a generator, a generator-based coroutine, or an async coroutine, the generated function will adapt. Fixes [#6](https://github.com/smarie/python-makefun/issues/6). ### 1.0.2 - Fixed `@with_signature` Now a string signature can be provided to `@with_signature` without problem. Fixed [#17](https://github.com/smarie/python-makefun/issues/17). ### 1.0.1 - minor: fixed PyPi doc ### 1.0.0 - New parameters, new goodie, and bugfix `@with_signature` : - now exposes all options of `create_function`. Fixed [#12](https://github.com/smarie/python-makefun/issues/12). - now correctly sets the module name by default. Fixes [#13](https://github.com/smarie/python-makefun/issues/13) - now accepts `None` as the new `func_signature` to declare that the signature is identical to the decorated function. This can be handy to just change the docstring or module name of a function for example. Fixes [#15](https://github.com/smarie/python-makefun/issues/15) `create_function` and `@with_signature`: - New `module_name` parameter to override the module name. Fixes [#14](https://github.com/smarie/python-makefun/issues/14) - the handler is now available as a field of the generated function (under `__func_impl__`). New `addhandler` parameter (default: True) controls this behaviour. Fixes [#16](https://github.com/smarie/python-makefun/issues/16) Misc: - New goodie to manipulate signatures: `add_signature_parameters`. - Fixed dependencies for documentation auto-build. ### 0.5.0 - New helper function, and bugfix New helper function `remove_signature_parameters`. Fixed issue with `@with_signature` when argument is a `Signature`. Fixes [#11](https://github.com/smarie/python-makefun/issues/11) ### 0.4.0 - New `@with_signature` decorator, and `create_function` accepts functions New decorator `@with_signature` to change the signature of a callable. Fixes [#3](https://github.com/smarie/python-makefun/issues/3) `create_function` now accepts that a function be passed as a signature template. Fixes [#10](https://github.com/smarie/python-makefun/issues/10) ### 0.3.0 - Ability to generate functions from `Signature` Functions can now be created from a `Signature` object, in addition to string signatures. This unlocks many useful use cases, among easily creating function wrappers. Note: the inner function that provides this feature is `get_signature_from_string`. Fixes [#8](https://github.com/smarie/python-makefun/issues/8) Improved design by getting rid of the regular expression parser to check parameters definition. This assumes that the compiler will correctly raise exceptions when a string signature is not correct, and that `inspect.signature` or `funcsigs.signature` works correctly at detecting all the parameter kinds and annotations on the resulting function. It seems like a fair assumption... Fixes [#9](https://github.com/smarie/python-makefun/issues/9). ### 0.2.0 - Various new features and improvements `create_function`: - `create_function` does not require users to prepend `"def "` to the signature anymore. Fixed [#5](https://github.com/smarie/python-makefun/issues/5) - Return annotations are now supported. Fixes [#4](https://github.com/smarie/python-makefun/issues/4). - Type hint as comments are supported but the generated function loses the annotations because `inspect.signature` loses the annotation too in that case. Fixes [#7](https://github.com/smarie/python-makefun/issues/7) - Variable-length arguments such as `*args` and `**kwargs` are now properly handled. Fixes [#2](https://github.com/smarie/python-makefun/issues/2) - Handler functions can now receive the dynamically created function as first argument, by using `create_function(func_signature, func_handler, inject_as_first_arg=True)`. Fixes [#1](https://github.com/smarie/python-makefun/issues/1) - Renamed `_call_` into `_func_impl_` in the generated code. Misc: - Added `pytest-cases` dependency for tests. ### 0.1.0 - First public version First version created, largely inspired by [`decorator`](https://github.com/micheles/decorator) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/docs/index.md�����������������������������������������������������������������0000664�0000000�0000000�00000054122�14644215064�0017142�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# makefun *Dynamically create python functions with a proper signature.* [![Python versions](https://img.shields.io/pypi/pyversions/makefun.svg)](https://pypi.python.org/pypi/makefun/) [![Build Status](https://github.com/smarie/python-makefun/actions/workflows/base.yml/badge.svg)](https://github.com/smarie/python-makefun/actions/workflows/base.yml) [![Tests Status](./reports/junit/junit-badge.svg?dummy=8484744)](./reports/junit/report.html) [![Coverage Status](./reports/coverage/coverage-badge.svg?dummy=8484744)](./reports/coverage/index.html) [![codecov](https://codecov.io/gh/smarie/python-makefun/branch/main/graph/badge.svg)](https://codecov.io/gh/smarie/python-makefun) [![Flake8 Status](./reports/flake8/flake8-badge.svg?dummy=8484744)](./reports/flake8/index.html) [![Documentation](https://img.shields.io/badge/doc-latest-blue.svg)](https://smarie.github.io/python-makefun/) [![PyPI](https://img.shields.io/pypi/v/makefun.svg)](https://pypi.python.org/pypi/makefun/) [![Downloads](https://pepy.tech/badge/makefun)](https://pepy.tech/project/makefun) [![Downloads per week](https://pepy.tech/badge/makefun/week)](https://pepy.tech/project/makefun) [![GitHub stars](https://img.shields.io/github/stars/smarie/python-makefun.svg)](https://github.com/smarie/python-makefun/stargazers) !!! success "New `prepend_args`, `append_args`, and `remove_args` parameters in [`@wraps`](./api_reference.md#wraps). See [below](#easier-edits) for details !" `makefun` helps you create functions dynamically, with the signature of your choice. It was largely inspired by [`decorator`](https://github.com/micheles/decorator) and `functools`, and created mainly to cover some of their limitations. The typical use cases are: - creating [**signature-preserving function wrappers**](#signature-preserving-function-wrappers) - just like `functools.wraps` but with accurate `TypeError` exception raising when user-provided arguments are wrong, and with a very convenient way to access argument values. - creating **function wrappers that have more or less arguments** that the function they wrap. A bit like `functools.partial` but a lot more flexible and friendly for your users. For example, I use it in [my pytest plugins](https://github.com/smarie/OVERVIEW#tests) to add a `requests` parameter to users' tests or fixtures when they do not already have it. - more generally, creating **functions with a signature derived from a reference signature**, - or even creating **functions with a signature completely defined at runtime**. It currently supports three ways to define the signature of the created function - from a given reference function, e.g. `foo`. - from strings, e.g. `'foo(a, b=1)'` - from `Signature` objects, either manually created, or obtained by using the `inspect.signature` (or its backport `funcsigs.signature`) method. !!! note "creating signature-preserving decorators" Creating decorators and creating signature-preserving function wrappers are two independent problems. `makefun` is solely focused on the second problem. If you wish to solve the first problem you can look at [`decopatch`](https://smarie.github.io/python-decopatch/). It provides a compact syntax, relying on `makefun`, if you wish to tackle both at once. ## Installing ```bash > pip install makefun ``` ## Usage ### 1- Ex-nihilo creation Let's create a function `foo(b, a=0)` implemented by `func_impl`. The easiest way to provide the signature is as a `str`: ```python from makefun import create_function # (1) define the signature. Warning: do not put 'def' keyword here! func_sig = "foo(b, a=0)" # (2) define the function implementation def func_impl(*args, **kwargs): """This docstring will be used in the generated function by default""" print("func_impl called !") return args, kwargs # (3) create the dynamic function gen_func = create_function(func_sig, func_impl) ``` We can test it: ```python >>> args, kwargs = gen_func(2) func_impl called ! >>> assert args == () >>> assert kwargs == {'a': 0, 'b': 2} ``` You can also: * remove the name from the signature string (e.g. `'(b, a=0)'`) to directly use the function name of `func_impl`. * override the function name, docstring, qualname and module name if you pass a non-None `func_name`, `doc`, `qualname` and `module_name` argument * add other attributes on the generated function if you pass additional keyword arguments See [API reference](./api_reference.md#create_function) for details. #### Arguments mapping We can see above that `args` is empty, even if we called `gen_func` with a positional argument. This is completely normal: this is because the created function does not expose `(*args, **kwargs)` but exposes the desired signature `(b, a=0)`. So as for usual python function calls, we lose the information about what was provided as positional and what was provided as keyword. You can try it yourself: write a function `def foo(b, a=0)` and now try to guess from the function body what was provided as positional, and what was provided as keyword... This behaviour is actually a great feature because it makes it much easier to develop the `func_impl`! Indeed, except if your desired signature contains *positional-only* (not yet available as of python 3.7) or *var-positional* (e.g. `*args`) arguments, you will **always** find all named arguments in `**kwargs`. #### More compact syntax You can use the [`@with_signature`](./api_reference.md#with_signature) decorator to perform exactly the same things than [`create_function`](./api_reference.md#create_function), but in a more compact way: ```python from makefun import with_signature @with_signature("foo(b, a=0)") def gen_func(*args, **kwargs): """This docstring will be used in the generated function by default""" print("func_impl called !") return args, kwargs ``` It also has the capability to take `None` as a signature, if you just want to update the metadata (`func_name`, `doc`, `qualname`, `module_name`) without creating any function: ```python @with_signature(None, func_name='f') def foo(a): return a assert foo.__name__ == 'f' ``` See [API reference](./api_reference.md#with_signature) for details. #### PEP484 type hints in `str` PEP484 type hints are supported in string function definitions: ```python func_sig = "foo(b: int, a: float = 0) -> str" ``` PEP484 type comments are also supported: ```python func_signature = """ foo(b, # type: int a = 0, # type: float ): # type: (...) -> str """ ``` but unfortunately `inspect.signature` is not able to detect them so the generated function does not contain the annotations. See [this example](https://github.com/smarie/python-makefun/issues/7#issuecomment-459353197). #### Using `Signature` objects [`create_function`](./api_reference.md#create_function) and [`@with_signature`](./api_reference.md#with_signature) are able to accept a `Signature` object as input, instead of a `str`. That might be more convenient than using strings to programmatically define signatures. For example we can rewrite the above script using `Signature`: ```python from makefun import with_signature from inspect import Signature, Parameter # (1) define the signature using objects. parameters = [Parameter('b', kind=Parameter.POSITIONAL_OR_KEYWORD), Parameter('a', kind=Parameter.POSITIONAL_OR_KEYWORD, default=0), ] func_sig = Signature(parameters) func_name = 'foo' # (2) define the function @with_signature(func_sig, func_name=func_name) def gen_func(*args, **kwargs): """This docstring will be used in the generated function by default""" print("func_impl called !") return args, kwargs ``` Note that `Signature` objects do not contain any function name information. You therefore have to provide an explicit `func_name` argument to [`@with_signature`](./api_reference.md#with_signature) (or to [`create_function`](./api_reference.md#create_function)) as shown above. !!! note "`Signature` availability in python 2" In python 2 the `inspect` package does not provide any signature-related features, but a complete backport is available: [`funcsigs`](https://github.com/testing-cabal/funcsigs). ### 2- Deriving from existing signatures In many real-world applications we want to reuse "as is", or slightly modify, an existing signature. #### Copying a signature If you just want to expose the same signature as a reference function (and not wrap it nor appear like it), the easiest way to copy the signature from another function `f` is to use `signature(f)` from `inspect`/`funcsigs`. #### Signature-preserving function wrappers [`@functools.wraps`](https://docs.python.org/3/library/functools.html#functools.wraps) is a famous decorator to create "signature-preserving" function wrappers. However it does not actually preserve the signature, it just uses a trick (setting the `__wrapped__` attribute) to trigger special dedicated behaviour in `stdlib`'s `help()` and `signature()` methods. See [here](https://stackoverflow.com/questions/308999/what-does-functools-wraps-do/55102697#55102697). This has two major limitations: 1. the wrapper code will execute *even when the provided arguments are invalid*. 2. the wrapper code can not easily access an argument using its name, from the received `*args, **kwargs`. Indeed one would have to handle all cases (positional, keyword, default) and therefore to use something like `Signature.bind()`. `makefun` provides a convenient replacement for [`@wraps`](./api_reference.md#wraps) that fixes these two issues: ```python from makefun import wraps # a dummy function def foo(a, b=1): """ foo doc """ return a + b # our signature-preserving wrapper @wraps(foo) def enhanced_foo(*args, **kwargs): print('hello!') print('b=%s' % kwargs['b']) # we can reliably access 'b' return foo(*args, **kwargs) ``` We can check that the wrapper behaves correctly whatever the call modes: ```python >>> assert enhanced_foo(1, 2) == 3 # positional 'b' hello! b=2 >>> assert enhanced_foo(b=0, a=1) == 1 # keyword 'b' hello! b=0 >>> assert enhanced_foo(1) == 2 # default 'b' hello! b=1 ``` And let's pass wrong arguments to it: we see that the wrapper is **not** executed. ```python >>> enhanced_foo() TypeError: foo() missing 1 required positional argument: 'a' ``` You can try to do the same experiment with `functools.wraps` to see the difference. Finally note that a [`create_wrapper`](./api_reference.md#create_wrapper) function is also provided for convenience ; it is the equivalent of [`@wraps`](./api_reference.md#wraps) but as a standard function - not a decorator. !!! note "creating signature-preserving decorators" Creating decorators and creating signature-preserving function wrappers are two independent problems. `makefun` is solely focused on the second problem. If you wish to solve the first problem you can look at [`decopatch`](https://smarie.github.io/python-decopatch/). It provides a compact syntax, relying on `makefun`, if you wish to tackle both at once. #### Editing a signature Below we show how to add a parameter to a function. We first capture its `Signature` using `inspect.signature(f)`, we modify it to add a parameter, and finally we use it in `wraps` to create our final function: ```python from makefun import wraps from inspect import signature, Parameter # (0) the reference function def foo(b, a=0): print("foo called: b=%s, a=%s" % (b, a)) return b, a # (1a) capture the signature of reference function `foo` foo_sig = signature(foo) print("Original Signature: %s" % foo_sig) # (1b) modify the signature to add a new parameter 'z' as first argument params = list(foo_sig.parameters.values()) params.insert(0, Parameter('z', kind=Parameter.POSITIONAL_OR_KEYWORD)) new_sig = foo_sig.replace(parameters=params) print("New Signature: %s" % new_sig) # (2) define the wrapper implementation @wraps(foo, new_sig=new_sig) def foo_wrapper(z, *args, **kwargs): print("foo_wrapper called ! z=%s" % z) # call the foo function output = foo(*args, **kwargs) # return augmented output return z, output # call it assert foo_wrapper(3, 2) == (3, (2, 0)) ``` yields ``` Original Signature: (b, a=0) New Signature: (z, b, a=0) foo_wrapper called ! z=3 foo called: b=2, a=0 ``` This way you can therefore easily create function wrappers with different signatures: not only adding, but also removing parameters, changing their kind (forcing keyword-only for example), etc. The possibilities are as endless as the capabilities of the `Signature` objects. Two helper functions are provided in this toolbox to make it a bit easier for you to edit `Signature` objects: - [`remove_signature_parameters`](./api_reference.md#remove_signature_parameters) creates a new signature from an existing one by removing all parameters corresponding to the names provided - [`add_signature_parameters`](./api_reference.md#add_signature_parameters) prepends the `Parameter`s provided in its `first=` argument, and appends the ones provided in its `last` argument. ```python from makefun import add_signature_parameters, remove_signature_parameters def foo(b, c, a=0): pass # original signature foo_sig = signature(foo) print("original signature: %s" % foo_sig) # let's modify it new_sig = add_signature_parameters(foo_sig, first=Parameter('z', kind=Parameter.POSITIONAL_OR_KEYWORD), last=Parameter('o', kind=Parameter.POSITIONAL_OR_KEYWORD, default=True) ) new_sig = remove_signature_parameters(new_sig, 'b', 'a') print("modified signature: %s" % new_sig) ``` yields ```bash original signature: (b, c, a=0) modified signature: (z, c, o=True) ``` They might save you a few lines of code if your use-case is not too specific. ##### Easier edits Now [`@wraps`](./api_reference.md#wraps) supports three new parameters to easily add or remove parameters to a signature: `append_args`, `prepend_args`, and `remove_args`. The above example can therefore be simplified to ```python from makefun import wraps def foo(b, a=0): print("foo called: b=%s, a=%s" % (b, a)) return b, a @wraps(foo, prepend_args='z') def foo_wrapper(z, *args, **kwargs): print("foo_wrapper called ! z=%s" % z) # call the foo function output = foo(*args, **kwargs) # return augmented output return z, output # call it assert foo_wrapper(3, 2) == (3, (2, 0)) ``` See [api documentation](./api_reference.md#wraps) for details. #### Removing parameters easily ##### To replace them with a hardcoded value As goodies, `makefun` provides a [`partial`](./api_reference.md#partial) function that are equivalent to [`functools.partial`](https://docs.python.org/2/library/functools.html#functools.partial), except that it is fully signature-preserving and modifies the documentation with a nice helper message explaining that this is a partial view: ```python def foo(x, y): """ a `foo` function :param x: :param y: :return: """ return x + y from makefun import partial bar = partial(foo, x=12) ``` we can test it: ```python >>> assert bar(1) == 13 >>> help(bar) Help on function bar in module makefun.tests.test_partial_and_macros: bar(y) <This function is equivalent to 'foo(y, x=12)', see original 'foo' doc below.> a `foo` function :param x: :param y: :return: ``` A [`@with_partial`](./api_reference.md#with_partial) decorator is also available to create partial views easily for quick tests: ```python @with_partial(x=12) def foo(x, y): """ a `foo` function :param x: :param y: :return: """ return x + y ``` ##### To inject a dynamically baked value As mentioned [previously](#easier-edits), [`@wraps`](./api_reference.md#wraps) provides a `remove_args` parameter where you can pass one or several argument names. ```python def inject_random_a(f): """ A decorator that injects a random number inside the `a` argument, removing it from the exposed signature """ @wraps(f, remove_args='a') def my_wrapper(*args, **kwargs): # generate a random value for a and inject it in the args for f kwargs['a'] = random() return f(*args, **kwargs) return my_wrapper @inject_random_a def summer(a, b): return a + b assert 12 <= summer(b=12) <= 13 ``` ### 3- Advanced topics #### Generators and Coroutines [`create_function`](./api_reference.md#create_function) and [`@with_signature`](./api_reference.md#with_signature) will automatically create a generator if your implementation is a generator: ```python # define the implementation def my_generator_impl(b, a=0): for i in range(a, b): yield i * i # create the dynamic function gen_func = create_function("foo(a, b)", my_generator_impl) # verify that the new function is a generator and behaves as such assert isgeneratorfunction(gen_func) assert list(gen_func(1, 4)) == [1, 4, 9] ``` The same goes for generator-based coroutines: ```python # define the impl that should be called def my_gencoroutine_impl(first_msg): second_msg = (yield first_msg) yield second_msg # create the dynamic function gen_func = create_function("foo(first_msg='hello')", my_gencoroutine_impl) # verify that the new func is a generator-based coroutine and behaves correctly cor = gen_func('hi') assert next(cor) == 'hi' assert cor.send('chaps') == 'chaps' cor.send('ola') # raises StopIteration ``` and asyncio coroutines as well ```python # define the impl that should be called async def my_native_coroutine_impl(sleep_time): await sleep(sleep_time) return sleep_time # create the dynamic function gen_func = create_function("foo(sleep_time=2)", my_native_coroutine_impl) # verify that the new function is a native coroutine and behaves correctly from asyncio import get_event_loop out = get_event_loop().run_until_complete(gen_func(5)) assert out == 5 ``` #### Generated source code The generated source code is in the `__source__` field of the generated function: ```python print(gen_func.__source__) ``` prints the following source code: ```python def foo(b, a=0): return _func_impl_(b=b, a=a) ``` The `_func_impl_` symbol represents your implementation. As [already mentioned](#arguments_mapping), you see that the variables are passed to it *as keyword arguments* when possible (`_func_impl_(b=b)`, not simply `_func_impl_(b)`). Of course if it is not possible it adapts: ```python gen_func = create_function("foo(a=0, *args, **kwargs)", func_impl) print(gen_func.__source__) ``` prints the following source code: ```python def foo(a=0, *args, **kwargs): return _func_impl_(a=a, *args, **kwargs) ``` #### Function reference injection In some scenarios you may wish to share the same implementation among several created functions, for example to expose slightly different signatures on top of the same core. In that case you may wish your implementation to know from which dynamically generated function it is being called. For this, simply use `inject_as_first_arg=True`, and the called function will be injected as the first argument: ```python def core_impl(f, *args, **kwargs): print("This is generic core called by %s" % f.__name__) # here you could use f.__name__ in a if statement to determine what to do if f.__name__ == "func1": print("called from func1 !") return args, kwargs # generate 2 functions func1 = create_function("func1(a, b)", core_impl, inject_as_first_arg=True) func2 = create_function("func2(a, d)", core_impl, inject_as_first_arg=True) func1(1, 2) func2(1, 2) ``` yields ``` This is generic core called by func1 called from func1 ! This is generic core called by func2 ``` ### 4. Other goodies #### `@compile_fun` A draft decorator to `compile` any existing function so that users cant debug through it. It can be handy to mask some code from your users for convenience (note that this does not provide any obfuscation, people can still reverse engineer your code easily. Actually the source code even gets copied in the function's `__source__` attribute for convenience): ```python from makefun import compile_fun @compile_fun def foo(a, b): return a + b assert foo(5, -5.0) == 0 print(foo.__source__) ``` yields ``` @compile_fun def foo(a, b): return a + b ``` If the function closure includes functions, they are recursively replaced with compiled versions too (only for this closure, this does not modify them otherwise). You may disable this behaviour entirely with `recurse=False`, or exclude some symbols from this recursion with the `except_names=(...)` arg (a tuple of names to exclude). **IMPORTANT** this decorator is a "goodie" in early stage and has not been extensively tested. Feel free to contribute ! Note that according to [this post](https://stackoverflow.com/a/471227/7262247) compiling does not make the code run any faster. Known issues: `NameError` may appear if your function code depends on symbols that have not yet been defined. Make sure all symbols exist first ! See [this issue](https://github.com/smarie/python-makefun/issues/47). ## Main features / benefits * **Generate functions with a dynamically defined signature**: the signature can be provided as a string or as a `Signature` object, thus making it handy to derive from other functions. * **Implement them easily**: the generated functions redirect their calls to the provided implementation function. As long as the signature is compliant, it will work as expected. For example the signature can be specific (`a: int, b=None`), and the implementation more generic (`*args, **kwargs`). Arguments will always be passed as keywords arguments when possible. * Replace **`@functools.wraps** so that it correctly preserves signatures, and enable you to easily access named arguments. ## See Also - [decorator](https://github.com/micheles/decorator), which largely inspired this code - [PEP362 - Function Signature Object](https://www.python.org/dev/peps/pep-0362) - [A blog entry on dynamic function creation](http://block.arch.ethz.ch/blog/2016/11/creating-functions-dynamically/) - [functools.wraps](https://docs.python.org/3/library/functools.html#functools.wraps) ### Others *Do you like this library ? You might also like [my other python libraries](https://github.com/smarie/OVERVIEW#python)* ## Want to contribute ? Details on the github page: [https://github.com/smarie/python-makefun](https://github.com/smarie/python-makefun) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/docs/long_description.md������������������������������������������������������0000664�0000000�0000000�00000003363�14644215064�0021376�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# makefun [![Python versions](https://img.shields.io/pypi/pyversions/makefun.svg)](https://pypi.python.org/pypi/makefun/) [![Build Status](https://github.com/smarie/python-makefun/actions/workflows/base.yml/badge.svg)](https://github.com/smarie/python-makefun/actions/workflows/base.yml) [![Tests Status](https://smarie.github.io/python-makefun/reports/junit/junit-badge.svg?dummy=8484744)](https://smarie.github.io/python-makefun/reports/junit/report.html) [![Coverage Status](https://smarie.github.io/python-makefun/reports/coverage/coverage-badge.svg?dummy=8484744)](https://smarie.github.io/python-makefun/reports/coverage/index.html) [![codecov](https://codecov.io/gh/smarie/python-makefun/branch/main/graph/badge.svg)](https://codecov.io/gh/smarie/python-makefun) [![Flake8 Status](https://smarie.github.io/python-makefun/reports/flake8/flake8-badge.svg?dummy=8484744)](https://smarie.github.io/python-makefun/reports/flake8/index.html) [![Documentation](https://img.shields.io/badge/doc-latest-blue.svg)](https://smarie.github.io/python-makefun/) [![PyPI](https://img.shields.io/pypi/v/makefun.svg)](https://pypi.python.org/pypi/makefun/) [![Downloads](https://pepy.tech/badge/makefun)](https://pepy.tech/project/makefun) [![Downloads per week](https://pepy.tech/badge/makefun/week)](https://pepy.tech/project/makefun) [![GitHub stars](https://img.shields.io/github/stars/smarie/python-makefun.svg)](https://github.com/smarie/python-makefun/stargazers) Small library to dynamically create python functions. The documentation for users is available here: [https://smarie.github.io/python-makefun/](https://smarie.github.io/python-makefun/) A readme for developers is available here: [https://github.com/smarie/python-makefun](https://github.com/smarie/python-makefun) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/mkdocs.yml��������������������������������������������������������������������0000664�0000000�0000000�00000001455�14644215064�0016565�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������site_name: makefun # site_description: 'A short description of my project' repo_url: https://github.com/smarie/python-makefun # docs_dir: docs # site_dir: ../site # default branch is main instead of master now on github edit_uri : ./edit/main/docs nav: - Home: index.md - API reference: api_reference.md - Changelog: changelog.md # - Advanced Usage: advanced_usage.md theme: material # readthedocs mkdocs markdown_extensions: - pymdownx.highlight # see https://squidfunk.github.io/mkdocs-material/reference/code-blocks/#highlight - pymdownx.superfences # same as above as well as code blocks inside other blocks - admonition # to add notes such as http://squidfunk.github.io/mkdocs-material/extensions/admonition/ # - codehilite: # guess_lang: false - toc: permalink: true �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/noxfile-requirements.txt������������������������������������������������������0000664�0000000�0000000�00000000144�14644215064�0021502�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������virtualenv nox toml makefun setuptools_scm # used in 'release' keyring # used in 'release' ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/noxfile.py��������������������������������������������������������������������0000664�0000000�0000000�00000030672�14644215064�0016603�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import argparse import json import logging import nox # noqa from pathlib import Path # noqa import sys # add parent folder to python path so that we can import noxfile_utils.py # note that you need to "pip install -r noxfile-requiterements.txt" for this file to work. sys.path.append(str(Path(__file__).parent / "ci_tools")) from nox_utils import (PY27, PY37, PY36, PY38, PY39, PY310, PY311, PY312, PY313, install_reqs, rm_folder, rm_file, DONT_INSTALL) # noqa pkg_name = "makefun" gh_org = "smarie" gh_repo = "python-makefun" # set the default activated sessions, minimal for CI nox.options.sessions = ["tests", "flake8", "docs"] # , "docs", "gh_pages" nox.options.error_on_missing_interpreters = True nox.options.reuse_existing_virtualenvs = True # this can be done using -r # if platform.system() == "Windows": >> always use this for better control nox.options.default_venv_backend = "virtualenv" # os.environ["NO_COLOR"] = "True" # nox.options.nocolor = True does not work # nox.options.verbose = True nox_logger = logging.getLogger("nox") # nox_logger.setLevel(logging.INFO) NO !!!! this prevents the "verbose" nox flag to work ! class Folders: root = Path(__file__).parent ci_tools = root / "ci_tools" runlogs = root / Path(nox.options.envdir or ".nox") / "_runlogs" runlogs.mkdir(parents=True, exist_ok=True) dist = root / "dist" site = root / "site" site_reports = site / "reports" reports_root = root / "docs" / "reports" test_reports = reports_root / "junit" test_xml = test_reports / "junit.xml" test_html = test_reports / "report.html" test_badge = test_reports / "junit-badge.svg" coverage_reports = reports_root / "coverage" coverage_xml = coverage_reports / "coverage.xml" coverage_intermediate_file = root / ".coverage" coverage_badge = coverage_reports / "coverage-badge.svg" flake8_reports = reports_root / "flake8" flake8_intermediate_file = root / "flake8stats.txt" flake8_badge = flake8_reports / "flake8-badge.svg" ENVS = { PY313: {"coverage": False, "pkg_specs": {"pip": ">19"}}, PY312: {"coverage": False, "pkg_specs": {"pip": ">19"}}, PY311: {"coverage": False, "pkg_specs": {"pip": ">19"}}, PY310: {"coverage": False, "pkg_specs": {"pip": ">19"}}, PY39: {"coverage": False, "pkg_specs": {"pip": ">19"}}, PY38: {"coverage": False, "pkg_specs": {"pip": ">19"}}, PY27: {"coverage": False, "pkg_specs": {"pip": ">10"}}, PY36: {"coverage": False, "pkg_specs": {"pip": ">19"}}, # IMPORTANT: this should be last so that the folder docs/reports is not deleted afterwards PY37: {"coverage": True, "pkg_specs": {"pip": ">19"}}, # , "pytest-html": "1.9.0" } ENV_PARAMS = tuple((k, v["coverage"], v["pkg_specs"]) for k, v in ENVS.items()) ENV_IDS = tuple(ENVS.keys()) @nox.session @nox.parametrize("python,coverage,pkg_specs", ENV_PARAMS, ids=ENV_IDS) def tests(session, coverage, pkg_specs): """Run the test suite, including test reports generation and coverage reports. """ # As soon as this runs, we delete the target site and coverage files to avoid reporting wrong coverage/etc. rm_folder(Folders.site) rm_folder(Folders.reports_root) # delete the .coverage files if any (they are not supposed to be any, but just in case) rm_file(Folders.coverage_intermediate_file) rm_file(Folders.root / "coverage.xml") # CI-only dependencies # Did we receive a flag through positional arguments ? (nox -s tests -- <flag>) # install_ci_deps = False # if len(session.posargs) == 1: # assert session.posargs[0] == "keyrings.alt" # install_ci_deps = True # elif len(session.posargs) > 1: # raise ValueError("Only a single positional argument is accepted, received: %r" % session.posargs) # uncomment and edit if you wish to uninstall something without deleting the whole env # session.run2("pip uninstall pytest-asyncio --yes") # install all requirements install_reqs(session, setup=True, install=True, tests=True, versions_dct=pkg_specs) # install CI-only dependencies # if install_ci_deps: # session.install2("keyrings.alt") # list all (conda list alone does not work correctly on github actions) # session.run2("conda list") # conda_prefix = Path(session.bin) # if conda_prefix.name == "bin": # conda_prefix = conda_prefix.parent # session.run2("conda list", env={"CONDA_PREFIX": str(conda_prefix), "CONDA_DEFAULT_ENV": session.get_session_id()}) # Fail if the assumed python version is not the actual one session.run("python", "ci_tools/check_python_version.py", session.python) # check that it can be imported even from a different folder # Important: do not surround the command into double quotes as in the shell ! # session.run('python', '-c', 'import os; os.chdir(\'./docs/\'); import %s' % pkg_name) # finally run all tests if not coverage: # install self so that it is recognized by pytest session.install(".", "--no-deps") # simple: pytest only session.run("python", "-m", "pytest", "--cache-clear", "-v", "tests/") else: # install self in "develop" mode so that coverage can be measured session.install("-e", ".", "--no-deps") # coverage + junit html reports + badge generation install_reqs(session, phase="coverage", phase_reqs=["coverage", "pytest-html", "genbadge[tests,coverage]"], versions_dct=pkg_specs) # --coverage + junit html reports session.run("coverage", "run", "--source", f"src/{pkg_name}", "-m", "pytest", "--cache-clear", f"--junitxml={Folders.test_xml}", f"--html={Folders.test_html}", "-v", "tests/") session.run("coverage", "report") session.run("coverage", "xml", "-o", f"{Folders.coverage_xml}") session.run("coverage", "html", "-d", f"{Folders.coverage_reports}") # delete this intermediate file, it is not needed anymore rm_file(Folders.coverage_intermediate_file) # --generates the badge for the test results and fail build if less than x% tests pass nox_logger.info("Generating badge for tests coverage") # Use our own package to generate the badge session.run("genbadge", "tests", "-i", f"{Folders.test_xml}", "-o", f"{Folders.test_badge}", "-t", "100") session.run("genbadge", "coverage", "-i", f"{Folders.coverage_xml}", "-o", f"{Folders.coverage_badge}") @nox.session(python=PY39) def flake8(session): """Launch flake8 qualimetry.""" session.install("-r", str(Folders.ci_tools / "flake8-requirements.txt")) session.run("pip", "install", ".") rm_folder(Folders.flake8_reports) Folders.flake8_reports.mkdir(parents=True, exist_ok=True) rm_file(Folders.flake8_intermediate_file) session.cd("src") # Options are set in `setup.cfg` file session.run("flake8", pkg_name, "--exit-zero", "--format=html", "--htmldir", str(Folders.flake8_reports), "--statistics", "--tee", "--output-file", str(Folders.flake8_intermediate_file)) # generate our badge session.run("genbadge", "flake8", "-i", f"{Folders.flake8_intermediate_file}", "-o", f"{Folders.flake8_badge}") rm_file(Folders.flake8_intermediate_file) @nox.session(python=PY39) def docs(session): """Generates the doc. Pass '-- serve' to serve it on a local http server instead.""" install_reqs(session, phase="docs", phase_reqs=["mkdocs-material", "mkdocs", "pymdown-extensions", "pygments"]) if session.posargs: # use posargs instead of "build" session.run("mkdocs", *session.posargs) else: session.run("mkdocs", "build") @nox.session(python=PY39) def publish(session): """Deploy the docs+reports on github pages. Note: this rebuilds the docs""" install_reqs(session, phase="publish", phase_reqs=["mkdocs-material", "mkdocs", "pymdown-extensions", "pygments"]) # possibly rebuild the docs in a static way (mkdocs serve does not build locally) session.run("mkdocs", "build") # check that the doc has been generated with coverage if not Folders.site_reports.exists(): raise ValueError("Test reports have not been built yet. Please run 'nox -s tests(3.7)' first") # publish the docs session.run("mkdocs", "gh-deploy") # publish the coverage - now in github actions only # install_reqs(session, phase="codecov", phase_reqs=["codecov", "keyring"]) # # keyring set https://app.codecov.io/gh/<org>/<repo> token # import keyring # (note that this import is not from the session env but the main nox env) # codecov_token = keyring.get_password("https://app.codecov.io/gh/<org>/<repo>>", "token") # # note: do not use --root nor -f ! otherwise "There was an error processing coverage reports" # session.run2('codecov -t %s -f %s' % (codecov_token, Folders.coverage_xml)) @nox.session(python=PY39) def release(session): """Create a release on github corresponding to the latest tag""" install_reqs(session, phase="setup.py#dist", phase_reqs=["setuptools_scm"]) # Get current tag using setuptools_scm and make sure this is not a dirty/dev one from setuptools_scm import get_version # (note that this import is not from the session env but the main nox env) from setuptools_scm.version import guess_next_dev_version version = [] def my_scheme(version_): version.append(version_) return guess_next_dev_version(version_) current_tag = get_version(".", version_scheme=my_scheme) # create the package rm_folder(Folders.dist) session.run("python", "setup.py", "sdist", "bdist_wheel") if version[0].dirty or not version[0].exact: raise ValueError("You need to execute this action on a clean tag version with no local changes.") # Did we receive a token through positional arguments ? (nox -s release -- <token>) if len(session.posargs) == 1: # Run from within github actions - no need to publish on pypi gh_token = session.posargs[0] publish_on_pypi = False elif len(session.posargs) == 0: # Run from local commandline - assume we want to manually publish on PyPi publish_on_pypi = True # keyring set https://docs.github.com/en/rest token import keyring # (note that this import is not from the session env but the main nox env) gh_token = keyring.get_password("https://docs.github.com/en/rest", "token") assert len(gh_token) > 0 else: raise ValueError("Only a single positional arg is allowed for now") # publish the package on PyPi if publish_on_pypi: # keyring set https://upload.pypi.org/legacy/ your-username # keyring set https://test.pypi.org/legacy/ your-username install_reqs(session, phase="PyPi", phase_reqs=["twine"]) session.run("twine", "upload", "dist/*", "-u", "smarie") # -r testpypi # create the github release install_reqs(session, phase="release", phase_reqs=["click", "PyGithub"]) session.run("python", "ci_tools/github_release.py", "-s", gh_token, "--repo-slug", f"{gh_org}/{gh_repo}", "-cf", "./docs/changelog.md", "-d", f"https://{gh_org}.github.io/{gh_repo}/changelog", current_tag) @nox.session(python=False) def gha_list(session): """(mandatory arg: <base_session_name>) Prints all sessions available for <base_session_name>, for GithubActions.""" # see https://stackoverflow.com/q/66747359/7262247 # The options parser = argparse.ArgumentParser() parser.add_argument("-s", "--session", help="The nox base session name") parser.add_argument( "-v", "--with_version", action="store_true", default=False, help="Return a list of lists where the first element is the python version and the second the nox session.", ) additional_args = parser.parse_args(session.posargs) # Now use --json CLI option out = session.run("nox", "-l", "--json", "-s", "tests", external=True, silent=True) sessions_list = [{"python": s["python"], "session": s["session"]} for s in json.loads(out)] # TODO filter # print the list so that it can be caught by GHA. # Note that json.dumps is optional since this is a list of string. # However it is to remind us that GHA expects a well-formatted json list of strings. print(json.dumps(sessions_list)) # if __name__ == '__main__': # # allow this file to be executable for easy debugging in any IDE # nox.run(globals()) ����������������������������������������������������������������������python-makefun-1.15.4/pyproject.toml����������������������������������������������������������������0000664�0000000�0000000�00000000430�14644215064�0017466�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[build-system] requires = [ "setuptools>=39.2", "setuptools_scm", "wheel" ] build-backend = "setuptools.build_meta" # pip: no ! does not work in old python 2.7 and not recommended here # https://setuptools.readthedocs.io/en/latest/userguide/quickstart.html#basic-use ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/setup.cfg���������������������������������������������������������������������0000664�0000000�0000000�00000011514�14644215064�0016400�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# See https://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files # And this great example : https://github.com/Kinto/kinto/blob/master/setup.cfg [metadata] name = makefun description = Small library to dynamically create python functions. description_file = README.md license = BSD 3-Clause long_description = file: docs/long_description.md long_description_content_type=text/markdown keywords = decorate decorator compile make dynamic function generate generation define definition signature args wrapper wraps author = Sylvain MARIE <sylvain.marie@se.com> maintainer = Sylvain MARIE <sylvain.marie@se.com> url = https://github.com/smarie/python-makefun # download_url = https://github.com/smarie/python-makefun/tarball/master >> do it in the setup.py to get the right version classifiers = # See https://pypi.python.org/pypi?%3Aaction=list_classifiers Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: BSD License Topic :: Software Development :: Libraries :: Python Modules Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 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 [options] # one day these will be able to come from requirement files, see https://github.com/pypa/setuptools/issues/1951. But will it be better ? setup_requires = setuptools_scm install_requires = # note: do not use double quotes in these, this triggers a weird bug in PyCharm in debug mode only funcsigs;python_version<'3.3' tests_require = pytest # for some reason these pytest dependencies were not declared in old versions of pytest six;python_version<'3.6' attr;python_version<'3.6' pluggy;python_version<'3.6' # test_suite = tests --> no need apparently # zip_safe = False # explicitly setting zip_safe=False to avoid downloading `ply` see https://github.com/smarie/python-getversion/pull/5 # and makes mypy happy see https://mypy.readthedocs.io/en/latest/installed_packages.html package_dir= =src packages = find: # see [options.packages.find] below # IMPORTANT: DO NOT set the `include_package_data` flag !! It triggers inclusion of all git-versioned files # see https://github.com/pypa/setuptools_scm/issues/190#issuecomment-351181286 # include_package_data = True [options.packages.find] where=src exclude = contrib docs *tests* [options.package_data] * = py.typed, *.pyi # Optional dependencies that can be installed with e.g. $ pip install -e .[dev,test] # [options.extras_require] # -------------- Packaging ----------- # [options.entry_points] # [egg_info] >> already covered by setuptools_scm [bdist_wheel] # Code is written to work on both Python 2 and Python 3. universal=1 # ------------- Others ------------- # In order to be able to execute 'python setup.py test' # from https://docs.pytest.org/en/latest/goodpractices.html#integrating-with-setuptools-python-setup-py-test-pytest-runner [aliases] test = pytest # pytest default configuration [tool:pytest] testpaths = tests/ addopts = --verbose --doctest-modules --ignore-glob='**/_*.py' # we need the 'always' for python 2 tests to work see https://github.com/pytest-dev/pytest/issues/2917 filterwarnings = always ; ignore::UserWarning # Coverage config [coverage:run] branch = True omit = *tests* # this is done in nox.py (github actions) or ci_tools/run_tests.sh (travis) # source = makefun # command_line = -m pytest --junitxml="reports/pytest_reports/pytest.xml" --html="reports/pytest_reports/pytest.html" -v makefun/tests/ [coverage:report] fail_under = 70 show_missing = True exclude_lines = # this line for all the python 2 not covered lines except ImportError: # we have to repeat this when exclude_lines is set pragma: no cover # Done in nox.py # [coverage:html] # directory = site/reports/coverage_reports # [coverage:xml] # output = site/reports/coverage_reports/coverage.xml [flake8] max-line-length = 120 extend-ignore = D, E203 # D: Docstring errors, E203: see https://github.com/PyCQA/pycodestyle/issues/373 copyright-check = True copyright-regexp = ^\#\s+Authors:\s+Sylvain MARIE <sylvain\.marie@se\.com>\n\#\s+\+\sAll\scontributors\sto\s<https://github\.com/smarie/python\-makefun>\n\#\n\#\s+License:\s3\-clause\sBSD,\s<https://github\.com/smarie/python\-makefun/blob/master/LICENSE> exclude = .git .github .nox .pytest_cache ci_tools docs tests noxfile.py setup.py */_version.py ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/setup.py����������������������������������������������������������������������0000664�0000000�0000000�00000002647�14644215064�0016300�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������""" To understand this project's build structure - This project uses setuptools, so it is declared as the build system in the pyproject.toml file - We use as much as possible `setup.cfg` to store the information so that it can be read by other tools such as `tox` and `nox`. So `setup.py` contains **almost nothing** (see below) This philosophy was found after trying all other possible combinations in other projects :) A reference project that was inspiring to make this move : https://github.com/Kinto/kinto/blob/master/setup.cfg See also: https://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files https://packaging.python.org/en/latest/distributing.html https://github.com/pypa/sampleproject """ from setuptools import setup # (1) check required versions (from https://medium.com/@daveshawley/safely-using-setup-cfg-for-metadata-1babbe54c108) import pkg_resources pkg_resources.require("setuptools>=39.2") pkg_resources.require("setuptools_scm") # (2) Generate download url using git version from setuptools_scm import get_version # noqa: E402 URL = "https://github.com/smarie/python-makefun" DOWNLOAD_URL = URL + "/tarball/" + get_version() # (3) Call setup() with as little args as possible setup( download_url=DOWNLOAD_URL, use_scm_version={ "write_to": "src/makefun/_version.py" }, # we can't put `use_scm_version` in setup.cfg yet unfortunately ) �����������������������������������������������������������������������������������������python-makefun-1.15.4/src/��������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14644215064�0015344�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/src/makefun/������������������������������������������������������������������0000775�0000000�0000000�00000000000�14644215064�0016772�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/src/makefun/__init__.py�������������������������������������������������������0000664�0000000�0000000�00000002331�14644215064�0021102�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Authors: Sylvain MARIE <sylvain.marie@se.com> # + All contributors to <https://github.com/smarie/python-makefun> # # License: 3-clause BSD, <https://github.com/smarie/python-makefun/blob/master/LICENSE> from .main import create_function, with_signature, remove_signature_parameters, add_signature_parameters, \ wraps, create_wrapper, partial, with_partial, compile_fun, UndefinedSymbolError, UnsupportedForCompilation, \ SourceUnavailable try: # -- Distribution mode: import from _version.py generated by setuptools_scm during release from ._version import version as __version__ except ImportError: # -- Source mode: use setuptools_scm to get the current version from src using git from setuptools_scm import get_version as _gv from os import path as _path __version__ = _gv(_path.join(_path.dirname(__file__), _path.pardir, _path.pardir)) __all__ = [ '__version__', # submodules 'main', # symbols 'create_function', 'with_signature', 'remove_signature_parameters', 'add_signature_parameters', 'wraps', 'create_wrapper', 'partial', 'with_partial', # pseudo compilation 'compile_fun', 'UndefinedSymbolError', 'UnsupportedForCompilation', 'SourceUnavailable' ] �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/src/makefun/_main_legacy_py.py������������������������������������������������0000664�0000000�0000000�00000007046�14644215064�0022472�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Authors: Sylvain MARIE <sylvain.marie@se.com> # + All contributors to <https://github.com/smarie/python-makefun> # # License: 3-clause BSD, <https://github.com/smarie/python-makefun/blob/master/LICENSE> import sys from itertools import chain from makefun.main import wraps def make_partial_using_yield(new_sig, f, *preset_pos_args, **preset_kwargs): """ Makes a 'partial' when f is a generator and python is new enough to support `yield from` :param new_sig: :param f: :param preset_pos_args: :param preset_kwargs: :return: """ @wraps(f, new_sig=new_sig) def partial_f(*args, **kwargs): # since the signature does the checking for us, no need to check for redundancy. kwargs.update(preset_kwargs) gen = f(*chain(preset_pos_args, args), **kwargs) _i = iter(gen) # initialize the generator _y = next(_i) # first iteration while 1: try: _s = yield _y # yield the first output and retrieve the new input except GeneratorExit as _e: # ---generator exit error--- try: _m = _i.close # if there is a close method except AttributeError: pass else: _m() # use it first raise _e # then re-raise exception except BaseException as _e: # ---other exception _x = sys.exc_info() # if captured exception, grab info try: _m = _i.throw # if there is a throw method except AttributeError: raise _e # otherwise re-raise else: _y = _m(*_x) # use it else: # --- nominal case: the new input was received # if _s is None: # _y = next(_i) # else: _y = _i.send(_s) # let the implementation decide if None means "no new input" or "new input = None" return partial_f def get_legacy_py_generator_body_template(): """ In Python 2 we cannot use `yield from` in the generated function body. This is a replacement, from PEP380 - see https://www.python.org/dev/peps/pep-0380/#formal-semantics note: we removed a few lines so that `StopIteration` exceptions are re-raised :return: """ return """def %s _i = iter(_func_impl_(%s)) # initialize the generator _y = next(_i) # first iteration while 1: try: _s = yield _y # yield the first output and retrieve the new input except GeneratorExit as _e: # ---generator exit error--- try: _m = _i.close # if there is a close method except AttributeError: pass else: _m() # use it first raise _e # then re-raise exception except BaseException as _e: # ---other exception _x = sys.exc_info() # if captured exception, grab info try: _m = _i.throw # if there is a throw method except AttributeError: raise _e # otherwise re-raise else: _y = _m(*_x) # use it else: # --- nominal case: the new input was received # if _s is None: # _y = next(_i) # else: _y = _i.send(_s) # let the implementation decide if None means "no new input" or "new input = None" """ ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/src/makefun/_main_py35_and_higher.py������������������������������������������0000664�0000000�0000000�00000001513�14644215064�0023457�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Authors: Sylvain MARIE <sylvain.marie@se.com> # + All contributors to <https://github.com/smarie/python-makefun> # # License: 3-clause BSD, <https://github.com/smarie/python-makefun/blob/master/LICENSE> from itertools import chain from makefun.main import wraps def make_partial_using_yield_from(new_sig, f, *preset_pos_args, **preset_kwargs): """ Makes a 'partial' when f is a generator and python is new enough to support `yield from` :param new_sig: :param f: :param presets: :return: """ @wraps(f, new_sig) def partial_f(*args, **kwargs): # since the signature does the checking for us, no need to check for redundancy. kwargs.update(preset_kwargs) # for python 3.4: explicit dict update yield from f(*chain(preset_pos_args, args), **kwargs) return partial_f �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/src/makefun/_main_py36_and_higher.py������������������������������������������0000664�0000000�0000000�00000001422�14644215064�0023457�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Authors: Sylvain MARIE <sylvain.marie@se.com> # + All contributors to <https://github.com/smarie/python-makefun> # # License: 3-clause BSD, <https://github.com/smarie/python-makefun/blob/master/LICENSE> from itertools import chain from makefun.main import wraps def make_partial_using_async_for_in_yield(new_sig, f, *preset_pos_args, **preset_kwargs): """ Makes a 'partial' when f is a async generator and python is new enough to support `async for v in f(): yield v` :param new_sig: :param f: :param presets: :return: """ @wraps(f, new_sig=new_sig) async def partial_f(*args, **kwargs): kwargs.update(preset_kwargs) async for v in f(*chain(preset_pos_args, args), **kwargs): yield v return partial_f ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/src/makefun/main.py�����������������������������������������������������������0000664�0000000�0000000�00000210440�14644215064�0020271�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Authors: Sylvain MARIE <sylvain.marie@se.com> # + All contributors to <https://github.com/smarie/python-makefun> # # License: 3-clause BSD, <https://github.com/smarie/python-makefun/blob/master/LICENSE> from __future__ import print_function import functools import re import sys import itertools from collections import OrderedDict from copy import copy from inspect import getsource from keyword import iskeyword from textwrap import dedent from types import FunctionType if sys.version_info >= (3, 0): is_identifier = str.isidentifier else: def is_identifier(string): """ Replacement for `str.isidentifier` when it is not available (e.g. on Python 2). :param string: :return: """ if len(string) == 0 or string[0].isdigit(): return False return all([(len(s) == 0) or s.isalnum() for s in string.split("_")]) try: # python 3.3+ from inspect import signature, Signature, Parameter except ImportError: from funcsigs import signature, Signature, Parameter try: from inspect import iscoroutinefunction except ImportError: # let's assume there are no coroutine functions in old Python def iscoroutinefunction(f): return False try: from inspect import isgeneratorfunction except ImportError: # assume no generator function in old Python versions def isgeneratorfunction(f): return False try: from inspect import isasyncgenfunction except ImportError: # assume no generator function in old Python versions def isasyncgenfunction(f): return False try: # python 3.5+ from typing import Callable, Any, Union, Iterable, Dict, Tuple, Mapping except ImportError: pass PY2 = sys.version_info < (3,) if not PY2: string_types = str, else: string_types = basestring, # noqa # macroscopic signature strings checker (we do not look inside params, `signature` will do it for us) FUNC_DEF = re.compile( '(?s)^\\s*(?P<funcname>[_\\w][_\\w\\d]*)?\\s*' '\\(\\s*(?P<params>.*?)\\s*\\)\\s*' '(((?P<typed_return_hint>->\\s*[^:]+)?(?P<colon>:)?\\s*)|:\\s*#\\s*(?P<comment_return_hint>.+))*$' ) def create_wrapper(wrapped, wrapper, new_sig=None, # type: Union[str, Signature] prepend_args=None, # type: Union[str, Parameter, Iterable[Union[str, Parameter]]] append_args=None, # type: Union[str, Parameter, Iterable[Union[str, Parameter]]] remove_args=None, # type: Union[str, Iterable[str]] func_name=None, # type: str inject_as_first_arg=False, # type: bool add_source=True, # type: bool add_impl=True, # type: bool doc=None, # type: str qualname=None, # type: str co_name=None, # type: str module_name=None, # type: str **attrs ): """ Creates a signature-preserving wrapper function. `create_wrapper(wrapped, wrapper, **kwargs)` is equivalent to `wraps(wrapped, **kwargs)(wrapper)`. See `@makefun.wraps` """ return wraps(wrapped, new_sig=new_sig, prepend_args=prepend_args, append_args=append_args, remove_args=remove_args, func_name=func_name, inject_as_first_arg=inject_as_first_arg, add_source=add_source, add_impl=add_impl, doc=doc, qualname=qualname, module_name=module_name, co_name=co_name, **attrs)(wrapper) def getattr_partial_aware(obj, att_name, *att_default): """ Same as getattr but recurses in obj.func if obj is a partial """ val = getattr(obj, att_name, *att_default) if isinstance(obj, functools.partial) and \ (val is None or att_name == '__dict__' and len(val) == 0): return getattr_partial_aware(obj.func, att_name, *att_default) else: return val def create_function(func_signature, # type: Union[str, Signature] func_impl, # type: Callable[[Any], Any] func_name=None, # type: str inject_as_first_arg=False, # type: bool add_source=True, # type: bool add_impl=True, # type: bool doc=None, # type: str qualname=None, # type: str co_name=None, # type: str module_name=None, # type: str **attrs): """ Creates a function with signature `func_signature` that will call `func_impl` when called. All arguments received by the generated function will be propagated as keyword-arguments to `func_impl` when it is possible (so all the time, except for var-positional or positional-only arguments that get passed as *args. Note that positional-only does not yet exist in python but this case is already covered because it is supported by `Signature` objects). `func_signature` can be provided in different formats: - as a string containing the name and signature without 'def' keyword, such as `'foo(a, b: int, *args, **kwargs)'`. In which case the name in the string will be used for the `__name__` and `__qualname__` of the created function by default - as a `Signature` object, for example created using `signature(f)` or handcrafted. Since a `Signature` object does not contain any name, in this case the `__name__` and `__qualname__` of the created function will be copied from `func_impl` by default. All the other metadata of the created function are defined as follows: - default `__name__` attribute (see above) can be overridden by providing a non-None `func_name` - default `__qualname__` attribute (see above) can be overridden by providing a non-None `qualname` - `__annotations__` attribute is created to match the annotations in the signature. - `__doc__` attribute is copied from `func_impl.__doc__` except if overridden using `doc` - `__module__` attribute is copied from `func_impl.__module__` except if overridden using `module_name` - `__code__.co_name` (see above) defaults to the same value as the above `__name__` attribute, except when that value is not a valid Python identifier, in which case it will be `<lambda>`. It can be overridden by providing a `co_name` that is either a valid Python identifier or `<lambda>`. Finally two new attributes are optionally created - `__source__` attribute: set if `add_source` is `True` (default), this attribute contains the source code of the generated function - `__func_impl__` attribute: set if `add_impl` is `True` (default), this attribute contains a pointer to `func_impl` A lambda function will be created in the following cases: - when `func_signature` is a `Signature` object and `func_impl` is itself a lambda function, - when the function name, either derived from a `func_signature` string, or given explicitly with `func_name`, is not a valid Python identifier, or - when the provided `co_name` is `<lambda>`. :param func_signature: either a string without 'def' such as "foo(a, b: int, *args, **kwargs)" or "(a, b: int)", or a `Signature` object, for example from the output of `inspect.signature` or from the `funcsigs.signature` backport. Note that these objects can be created manually too. If the signature is provided as a string and contains a non-empty name, this name will be used instead of the one of the decorated function. :param func_impl: the function that will be called when the generated function is executed. Its signature should be compliant with (=more generic than) `func_signature` :param inject_as_first_arg: if `True`, the created function will be injected as the first positional argument of `func_impl`. This can be handy in case the implementation is shared between several facades and needs to know from which context it was called. Default=`False` :param func_name: provide a non-`None` value to override the created function `__name__` and `__qualname__`. If this is `None` (default), the `__name__` will default to the one of `func_impl` if `func_signature` is a `Signature`, or to the name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name. :param add_source: a boolean indicating if a '__source__' annotation should be added to the generated function (default: True) :param add_impl: a boolean indicating if a '__func_impl__' annotation should be added to the generated function (default: True) :param doc: a string representing the docstring that will be used to set the __doc__ attribute on the generated function. If None (default), the doc of func_impl will be used. :param qualname: a string representing the qualified name to be used. If None (default), the `__qualname__` will default to the one of `func_impl` if `func_signature` is a `Signature`, or to the name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name. :param co_name: a string representing the name to be used in the compiled code of the function. If None (default), the `__code__.co_name` will default to the one of `func_impl` if `func_signature` is a `Signature`, or to the name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name. :param module_name: the name of the module to be set on the function (under __module__ ). If None (default), `func_impl.__module__` will be used. :param attrs: other keyword attributes that should be set on the function. Note that `func_impl.__dict__` is not automatically copied. :return: """ # grab context from the caller frame try: attrs.pop('_with_sig_') # called from `@with_signature` frame = _get_callerframe(offset=1) except KeyError: frame = _get_callerframe() evaldict, _ = extract_module_and_evaldict(frame) # name defaults user_provided_name = True if func_name is None: # allow None, this will result in a lambda function being created func_name = getattr_partial_aware(func_impl, '__name__', None) user_provided_name = False # co_name default user_provided_co_name = co_name is not None if not user_provided_co_name: if func_name is None: co_name = '<lambda>' else: co_name = func_name else: if not (_is_valid_func_def_name(co_name) or _is_lambda_func_name(co_name)): raise ValueError("Invalid co_name %r for created function. " "It is not possible to declare a function " "with the provided co_name." % co_name) # qname default user_provided_qname = True if qualname is None: qualname = getattr_partial_aware(func_impl, '__qualname__', None) user_provided_qname = False # doc default if doc is None: doc = getattr(func_impl, '__doc__', None) # note: as opposed to what we do in `@wraps`, we cannot easily generate a better doc for partials here. # Indeed the new signature may not easily match the one in the partial. # module name default if module_name is None: module_name = getattr_partial_aware(func_impl, '__module__', None) # input signature handling if isinstance(func_signature, str): # transform the string into a Signature and make sure the string contains ":" func_name_from_str, func_signature, func_signature_str = get_signature_from_string(func_signature, evaldict) # if not explicitly overridden using `func_name`, the name in the string takes over if func_name_from_str is not None: if not user_provided_name: func_name = func_name_from_str if not user_provided_qname: qualname = func_name if not user_provided_co_name: co_name = func_name create_lambda = not _is_valid_func_def_name(co_name) # if lambda, strip the name, parentheses and colon from the signature if create_lambda: name_len = len(func_name_from_str) if func_name_from_str else 0 func_signature_str = func_signature_str[name_len + 1: -2] # fix the signature if needed elif func_name_from_str is None: func_signature_str = co_name + func_signature_str elif isinstance(func_signature, Signature): # create the signature string create_lambda = not _is_valid_func_def_name(co_name) if create_lambda: # create signature string (or argument string in the case of a lambda function func_signature_str = get_lambda_argument_string(func_signature, evaldict) else: func_signature_str = get_signature_string(co_name, func_signature, evaldict) else: raise TypeError("Invalid type for `func_signature`: %s" % type(func_signature)) # extract all information needed from the `Signature` params_to_kw_assignment_mode = get_signature_params(func_signature) params_names = list(params_to_kw_assignment_mode.keys()) # Note: in decorator the annotations were extracted using getattr(func_impl, '__annotations__') instead. # This seems equivalent but more general (provided by the signature, not the function), but to check annotations, defaults, kwonlydefaults = get_signature_details(func_signature) # create the body of the function to compile # The generated function body should dispatch its received arguments to the inner function. # For this we will pass as much as possible the arguments as keywords. # However if there are varpositional arguments we cannot assignments = [("%s=%s" % (k, k)) if is_kw else k for k, is_kw in params_to_kw_assignment_mode.items()] params_str = ', '.join(assignments) if inject_as_first_arg: params_str = "%s, %s" % (func_name, params_str) if _is_generator_func(func_impl): if sys.version_info >= (3, 3): body = "def %s\n yield from _func_impl_(%s)\n" % (func_signature_str, params_str) else: from makefun._main_legacy_py import get_legacy_py_generator_body_template body = get_legacy_py_generator_body_template() % (func_signature_str, params_str) elif isasyncgenfunction(func_impl): body = "async def %s\n async for y in _func_impl_(%s):\n yield y\n" % (func_signature_str, params_str) elif create_lambda: if func_signature_str: body = "lambda_ = lambda %s: _func_impl_(%s)\n" % (func_signature_str, params_str) else: body = "lambda_ = lambda: _func_impl_(%s)\n" % (params_str) else: body = "def %s\n return _func_impl_(%s)\n" % (func_signature_str, params_str) if iscoroutinefunction(func_impl): body = ("async " + body).replace('return _func_impl_', 'return await _func_impl_') # create the function by compiling code, mapping the `_func_impl_` symbol to `func_impl` protect_eval_dict(evaldict, func_name, params_names) evaldict['_func_impl_'] = func_impl if create_lambda: f = _make("lambda_", params_names, body, evaldict) else: f = _make(co_name, params_names, body, evaldict) # add the source annotation if needed if add_source: attrs['__source__'] = body # add the handler if needed if add_impl: attrs['__func_impl__'] = func_impl # update the signature _update_fields(f, name=func_name, qualname=qualname, doc=doc, annotations=annotations, defaults=tuple(defaults), kwonlydefaults=kwonlydefaults, module=module_name, kw=attrs) return f def _is_generator_func(func_impl): """ Return True if the func_impl is a generator :param func_impl: :return: """ if (3, 5) <= sys.version_info < (3, 6): # with Python 3.5 isgeneratorfunction returns True for all coroutines # however we know that it is NOT possible to have a generator # coroutine in python 3.5: PEP525 was not there yet return isgeneratorfunction(func_impl) and not iscoroutinefunction(func_impl) else: return isgeneratorfunction(func_impl) def _is_lambda_func_name(func_name): """ Return True if func_name is the name of a lambda :param func_name: :return: """ return func_name == (lambda: None).__code__.co_name def _is_valid_func_def_name(func_name): """ Return True if func_name is valid in a function definition. :param func_name: :return: """ return is_identifier(func_name) and not iskeyword(func_name) class _SymbolRef: """ A class used to protect signature default values and type hints when the local context would not be able to evaluate them properly when the new function is created. In this case we store them under a known name, we add that name to the locals(), and we use this symbol that has a repr() equal to the name. """ __slots__ = 'varname' def __init__(self, varname): self.varname = varname def __repr__(self): return self.varname def get_signature_string(func_name, func_signature, evaldict): """ Returns the string to be used as signature. If there is a non-native symbol in the defaults, it is created as a variable in the evaldict :param func_name: :param func_signature: :return: """ no_type_hints_allowed = sys.version_info < (3, 5) # protect the parameters if needed new_params = [] params_changed = False for p_name, p in func_signature.parameters.items(): # if default value can not be evaluated, protect it default_needs_protection = _signature_symbol_needs_protection(p.default, evaldict) new_default = _protect_signature_symbol(p.default, default_needs_protection, "DEFAULT_%s" % p_name, evaldict) if no_type_hints_allowed: new_annotation = Parameter.empty annotation_needs_protection = new_annotation is not p.annotation else: # if type hint can not be evaluated, protect it annotation_needs_protection = _signature_symbol_needs_protection(p.annotation, evaldict) new_annotation = _protect_signature_symbol(p.annotation, annotation_needs_protection, "HINT_%s" % p_name, evaldict) # only create if necessary (inspect __init__ methods are slow) if default_needs_protection or annotation_needs_protection: # replace the parameter with the possibly new default and hint p = Parameter(p.name, kind=p.kind, default=new_default, annotation=new_annotation) params_changed = True new_params.append(p) if no_type_hints_allowed: new_return_annotation = Parameter.empty return_needs_protection = new_return_annotation is not func_signature.return_annotation else: # if return type hint can not be evaluated, protect it return_needs_protection = _signature_symbol_needs_protection(func_signature.return_annotation, evaldict) new_return_annotation = _protect_signature_symbol(func_signature.return_annotation, return_needs_protection, "RETURNHINT", evaldict) # only create new signature if necessary (inspect __init__ methods are slow) if params_changed or return_needs_protection: s = Signature(parameters=new_params, return_annotation=new_return_annotation) else: s = func_signature # return the final string representation return "%s%s:" % (func_name, s) def get_lambda_argument_string(func_signature, evaldict): """ Returns the string to be used as arguments in a lambda function definition. If there is a non-native symbol in the defaults, it is created as a variable in the evaldict :param func_name: :param func_signature: :return: """ return get_signature_string('', func_signature, evaldict)[1:-2] TYPES_WITH_SAFE_REPR = (int, str, bytes, bool) # IMPORTANT note: float is not in the above list because not all floats have a repr that is valid for the # compiler: float('nan'), float('-inf') and float('inf') or float('+inf') have an invalid repr. def _signature_symbol_needs_protection(symbol, evaldict): """ Helper method for signature symbols (defaults, type hints) protection. Returns True if the given symbol needs to be protected - that is, if its repr() can not be correctly evaluated with current evaldict. :param symbol: :return: """ if symbol is not None and symbol is not Parameter.empty and type(symbol) not in TYPES_WITH_SAFE_REPR: try: # check if the repr() of the default value is equal to itself. return eval(repr(symbol), evaldict) != symbol # noqa # we cannot use ast.literal_eval, too restrictive except Exception: # in case of error this needs protection return True else: return False def _protect_signature_symbol(val, needs_protection, varname, evaldict): """ Helper method for signature symbols (defaults, type hints) protection. Returns either `val`, or a protection symbol. In that case the protection symbol is created with name `varname` and inserted into `evaldict` :param val: :param needs_protection: :param varname: :param evaldict: :return: """ if needs_protection: # store the object in the evaldict and insert name evaldict[varname] = val return _SymbolRef(varname) else: return val def get_signature_from_string(func_sig_str, evaldict): """ Creates a `Signature` object from the given function signature string. :param func_sig_str: :return: (func_name, func_sig, func_sig_str). func_sig_str is guaranteed to contain the ':' symbol already """ # escape leading newline characters if func_sig_str.startswith('\n'): func_sig_str = func_sig_str[1:] # match the provided signature. note: fullmatch is not supported in python 2 def_match = FUNC_DEF.match(func_sig_str) if def_match is None: raise SyntaxError('The provided function template is not valid: "%s" does not match ' '"<func_name>(<func_args>)[ -> <return-hint>]".\n For information the regex used is: "%s"' '' % (func_sig_str, FUNC_DEF.pattern)) groups = def_match.groupdict() # extract function name and parameter names list func_name = groups['funcname'] if func_name is None or func_name == '': func_name_ = 'dummy' func_name = None else: func_name_ = func_name # params_str = groups['params'] # params_names = extract_params_names(params_str) # find the keyword parameters and the others # posonly_names, kwonly_names, unrestricted_names = separate_positional_and_kw(params_names) colon_end = groups['colon'] cmt_return_hint = groups['comment_return_hint'] if (colon_end is None or len(colon_end) == 0) \ and (cmt_return_hint is None or len(cmt_return_hint) == 0): func_sig_str = func_sig_str + ':' # Create a dummy function # complete the string if name is empty, so that we can actually use _make func_sig_str_ = (func_name_ + func_sig_str) if func_name is None else func_sig_str body = 'def %s\n pass\n' % func_sig_str_ dummy_f = _make(func_name_, [], body, evaldict) # return its signature return func_name, signature(dummy_f), func_sig_str # def extract_params_names(params_str): # return [m.groupdict()['name'] for m in PARAM_DEF.finditer(params_str)] # def separate_positional_and_kw(params_names): # """ # Extracts the names that are positional-only, keyword-only, or non-constrained # :param params_names: # :return: # """ # # by default all parameters can be passed as positional or keyword # posonly_names = [] # kwonly_names = [] # other_names = params_names # # # but if we find explicit separation we have to change our mind # for i in range(len(params_names)): # name = params_names[i] # if name == '*': # del params_names[i] # posonly_names = params_names[0:i] # kwonly_names = params_names[i:] # other_names = [] # break # elif name[0] == '*' and name[1] != '*': # # # that's a *args. Next one will be keyword-only # posonly_names = params_names[0:(i + 1)] # kwonly_names = params_names[(i + 1):] # other_names = [] # break # else: # # continue # pass # # return posonly_names, kwonly_names, other_names def get_signature_params(s): """ Utility method to return the parameter names in the provided `Signature` object, by group of kind :param s: :return: """ # this ordered dictionary will contain parameters and True/False whether we should use keyword assignment or not params_to_assignment_mode = OrderedDict() for p_name, p in s.parameters.items(): if p.kind is Parameter.POSITIONAL_ONLY: params_to_assignment_mode[p_name] = False elif p.kind is Parameter.KEYWORD_ONLY: params_to_assignment_mode[p_name] = True elif p.kind is Parameter.POSITIONAL_OR_KEYWORD: params_to_assignment_mode[p_name] = True elif p.kind is Parameter.VAR_POSITIONAL: # We have to pass all the arguments that were here in previous positions, as positional too. for k in params_to_assignment_mode.keys(): params_to_assignment_mode[k] = False params_to_assignment_mode["*" + p_name] = False elif p.kind is Parameter.VAR_KEYWORD: params_to_assignment_mode["**" + p_name] = False else: raise ValueError("Unknown kind: %s" % p.kind) return params_to_assignment_mode def get_signature_details(s): """ Utility method to extract the annotations, defaults and kwdefaults from a `Signature` object :param s: :return: """ annotations = dict() defaults = [] kwonlydefaults = dict() if s.return_annotation is not s.empty: annotations['return'] = s.return_annotation for p_name, p in s.parameters.items(): if p.annotation is not s.empty: annotations[p_name] = p.annotation if p.default is not s.empty: # if p_name not in kwonly_names: if p.kind is not Parameter.KEYWORD_ONLY: defaults.append(p.default) else: kwonlydefaults[p_name] = p.default return annotations, defaults, kwonlydefaults def extract_module_and_evaldict(frame): """ Utility function to extract the module name from the given frame, and to return a dictionary containing globals and locals merged together :param frame: :return: """ try: # get the module name module_name = frame.f_globals.get('__name__', '?') # construct a dictionary with all variables # this is required e.g. if a symbol is used in a type hint evaldict = copy(frame.f_globals) evaldict.update(frame.f_locals) except AttributeError: # either the frame is None of the f_globals and f_locals are not available module_name = '?' evaldict = dict() return evaldict, module_name def protect_eval_dict(evaldict, func_name, params_names): """ remove all symbols that could be harmful in evaldict :param evaldict: :param func_name: :param params_names: :return: """ try: del evaldict[func_name] except KeyError: pass for n in params_names: try: del evaldict[n] except KeyError: pass return evaldict # Atomic get-and-increment provided by the GIL _compile_count = itertools.count() def _make(funcname, params_names, body, evaldict=None): """ Make a new function from a given template and update the signature :param func_name: :param params_names: :param body: :param evaldict: :param add_source: :return: """ evaldict = evaldict or {} for n in params_names: if n in ('_func_', '_func_impl_'): raise NameError('%s is overridden in\n%s' % (n, body)) if not body.endswith('\n'): # newline is needed for old Pythons raise ValueError("body should end with a newline") # Ensure each generated function has a unique filename for profilers # (such as cProfile) that depend on the tuple of (<filename>, # <definition line>, <function name>) being unique. filename = '<makefun-gen-%d>' % (next(_compile_count),) try: code = compile(body, filename, 'single') exec(code, evaldict) # noqa except BaseException: print('Error in generated code:', file=sys.stderr) print(body, file=sys.stderr) raise # extract the function from compiled code func = evaldict[funcname] return func def _update_fields( func, name, qualname=None, doc=None, annotations=None, defaults=(), kwonlydefaults=None, module=None, kw=None ): """ Update the signature of func with the provided information This method merely exists to remind which field have to be filled. :param func: :param name: :param qualname: :param kw: :return: """ if kw is None: kw = dict() func.__name__ = name if qualname is not None: func.__qualname__ = qualname func.__doc__ = doc func.__dict__ = kw func.__defaults__ = defaults if len(kwonlydefaults) == 0: kwonlydefaults = None func.__kwdefaults__ = kwonlydefaults func.__annotations__ = annotations func.__module__ = module def _get_callerframe(offset=0): try: # inspect.stack is extremely slow, the fastest is sys._getframe or inspect.currentframe(). # See https://gist.github.com/JettJones/c236494013f22723c1822126df944b12 frame = sys._getframe(2 + offset) # frame = currentframe() # for _ in range(2 + offset): # frame = frame.f_back except AttributeError: # for IronPython and similar implementations frame = None return frame def wraps(wrapped_fun, new_sig=None, # type: Union[str, Signature] prepend_args=None, # type: Union[str, Parameter, Iterable[Union[str, Parameter]]] append_args=None, # type: Union[str, Parameter, Iterable[Union[str, Parameter]]] remove_args=None, # type: Union[str, Iterable[str]] func_name=None, # type: str co_name=None, # type: str inject_as_first_arg=False, # type: bool add_source=True, # type: bool add_impl=True, # type: bool doc=None, # type: str qualname=None, # type: str module_name=None, # type: str **attrs ): """ A decorator to create a signature-preserving wrapper function. It is similar to `functools.wraps`, but - relies on a proper dynamically-generated function. Therefore as opposed to `functools.wraps`, - the wrapper body will not be executed if the arguments provided are not compliant with the signature - instead a `TypeError` will be raised before entering the wrapper body. - the arguments will always be received as keywords by the wrapper, when possible. See [documentation](./index.md#signature-preserving-function-wrappers) for details. - you can modify the signature of the resulting function, by providing a new one with `new_sig` or by providing a list of arguments to remove in `remove_args`, to prepend in `prepend_args`, or to append in `append_args`. See [documentation](./index.md#editing-a-signature) for details. Comparison with `@with_signature`:`@wraps(f)` is equivalent to `@with_signature(signature(f), func_name=f.__name__, doc=f.__doc__, module_name=f.__module__, qualname=f.__qualname__, __wrapped__=f, **f.__dict__, **attrs)` In other words, as opposed to `@with_signature`, the metadata (doc, module name, etc.) is provided by the wrapped `wrapped_fun`, so that the created function seems to be identical (except possibly for the signature). Note that all options in `with_signature` can still be overridden using parameters of `@wraps`. The additional `__wrapped__` attribute is set on the created function, to stay consistent with the `functools.wraps` behaviour. If the signature is modified through `new_sig`, `remove_args`, `append_args` or `prepend_args`, the additional `__signature__` attribute will be set so that `inspect.signature` and related functionality works as expected. See PEP 362 for more detail on `__wrapped__` and `__signature__`. See also [python documentation on @wraps](https://docs.python.org/3/library/functools.html#functools.wraps) :param wrapped_fun: the function that you intend to wrap with the decorated function. As in `functools.wraps`, `wrapped_fun` is used as the default reference for the exposed signature, `__name__`, `__qualname__`, `__doc__` and `__dict__`. :param new_sig: the new signature of the decorated function. By default it is `None` and means "same signature as in `wrapped_fun`" (similar behaviour as in `functools.wraps`). If you wish to modify the exposed signature you can either use `remove/prepend/append_args`, or pass a non-None `new_sig`. It can be either a string without 'def' such as "foo(a, b: int, *args, **kwargs)" of "(a, b: int)", or a `Signature` object, for example from the output of `inspect.signature` or from the `funcsigs.signature` backport. Note that these objects can be created manually too. If the signature is provided as a string and contains a non-empty name, this name will be used instead of the one of `wrapped_fun`. :param prepend_args: a string or list of strings to prepend to the signature of `wrapped_fun`. These extra arguments should not be passed to `wrapped_fun`, as it does not know them. This is typically used to easily create a wrapper with additional arguments, without having to manipulate the signature objects. :param append_args: a string or list of strings to append to the signature of `wrapped_fun`. These extra arguments should not be passed to `wrapped_fun`, as it does not know them. This is typically used to easily create a wrapper with additional arguments, without having to manipulate the signature objects. :param remove_args: a string or list of strings to remove from the signature of `wrapped_fun`. These arguments should be injected in the received `kwargs` before calling `wrapped_fun`, as it requires them. This is typically used to easily create a wrapper with less arguments, without having to manipulate the signature objects. :param func_name: provide a non-`None` value to override the created function `__name__` and `__qualname__`. If this is `None` (default), the `__name__` will default to the ones of `wrapped_fun` if `new_sig` is `None` or is a `Signature`, or to the name defined in `new_sig` if `new_sig` is a `str` and contains a non-empty name. :param inject_as_first_arg: if `True`, the created function will be injected as the first positional argument of the decorated function. This can be handy in case the implementation is shared between several facades and needs to know from which context it was called. Default=`False` :param add_source: a boolean indicating if a '__source__' annotation should be added to the generated function (default: True) :param add_impl: a boolean indicating if a '__func_impl__' annotation should be added to the generated function (default: True) :param doc: a string representing the docstring that will be used to set the __doc__ attribute on the generated function. If None (default), the doc of `wrapped_fun` will be used. If `wrapped_fun` is an instance of `functools.partial`, a special enhanced doc will be generated. :param qualname: a string representing the qualified name to be used. If None (default), the `__qualname__` will default to the one of `wrapped_fun`, or the one in `new_sig` if `new_sig` is provided as a string with a non-empty function name. :param co_name: a string representing the name to be used in the compiled code of the function. If None (default), the `__code__.co_name` will default to the one of `func_impl` if `func_signature` is a `Signature`, or to the name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name. :param module_name: the name of the module to be set on the function (under __module__ ). If None (default), the `__module__` attribute of `wrapped_fun` will be used. :param attrs: other keyword attributes that should be set on the function. Note that the full `__dict__` of `wrapped_fun` is automatically copied. :return: a decorator """ func_name, func_sig, doc, qualname, co_name, module_name, all_attrs = _get_args_for_wrapping(wrapped_fun, new_sig, remove_args, prepend_args, append_args, func_name, doc, qualname, co_name, module_name, attrs) return with_signature(func_sig, func_name=func_name, inject_as_first_arg=inject_as_first_arg, add_source=add_source, add_impl=add_impl, doc=doc, qualname=qualname, co_name=co_name, module_name=module_name, **all_attrs) def _get_args_for_wrapping(wrapped, new_sig, remove_args, prepend_args, append_args, func_name, doc, qualname, co_name, module_name, attrs): """ Internal method used by @wraps and create_wrapper :param wrapped: :param new_sig: :param remove_args: :param prepend_args: :param append_args: :param func_name: :param doc: :param qualname: :param co_name: :param module_name: :param attrs: :return: """ # the desired signature has_new_sig = False if new_sig is not None: if remove_args is not None or prepend_args is not None or append_args is not None: raise ValueError("Only one of `[remove/prepend/append]_args` or `new_sig` should be provided") func_sig = new_sig has_new_sig = True else: func_sig = signature(wrapped) if remove_args: if isinstance(remove_args, string_types): remove_args = (remove_args,) func_sig = remove_signature_parameters(func_sig, *remove_args) has_new_sig = True if prepend_args: if isinstance(prepend_args, string_types): prepend_args = (prepend_args,) else: prepend_args = () if append_args: if isinstance(append_args, string_types): append_args = (append_args,) else: append_args = () if prepend_args or append_args: has_new_sig = True func_sig = add_signature_parameters(func_sig, first=prepend_args, last=append_args) # the desired metadata if func_name is None: func_name = getattr_partial_aware(wrapped, '__name__', None) if doc is None: doc = getattr(wrapped, '__doc__', None) if isinstance(wrapped, functools.partial) and not has_new_sig \ and doc == functools.partial(lambda: True).__doc__: # the default generic partial doc. Generate a better doc, since we know that the sig is not messed with orig_sig = signature(wrapped.func) doc = gen_partial_doc(getattr_partial_aware(wrapped.func, '__name__', None), getattr_partial_aware(wrapped.func, '__doc__', None), orig_sig, func_sig, wrapped.args) if qualname is None: qualname = getattr_partial_aware(wrapped, '__qualname__', None) if module_name is None: module_name = getattr_partial_aware(wrapped, '__module__', None) if co_name is None: code = getattr_partial_aware(wrapped, '__code__', None) if code is not None: co_name = code.co_name # attributes: start from the wrapped dict, add '__wrapped__' if needed, and override with all attrs. all_attrs = copy(getattr_partial_aware(wrapped, '__dict__')) # PEP362: always set `__wrapped__`, and if signature was changed, set `__signature__` too all_attrs["__wrapped__"] = wrapped if has_new_sig: if isinstance(func_sig, Signature): all_attrs["__signature__"] = func_sig else: # __signature__ must be a Signature object, so if it is a string we need to evaluate it. frame = _get_callerframe(offset=1) evaldict, _ = extract_module_and_evaldict(frame) # Here we could wish to directly override `func_name` and `func_sig` so that this does not have to be done # again by `create_function` later... Would this be risky ? _func_name, func_sig_as_sig, _ = get_signature_from_string(func_sig, evaldict) all_attrs["__signature__"] = func_sig_as_sig all_attrs.update(attrs) return func_name, func_sig, doc, qualname, co_name, module_name, all_attrs def with_signature(func_signature, # type: Union[str, Signature] func_name=None, # type: str inject_as_first_arg=False, # type: bool add_source=True, # type: bool add_impl=True, # type: bool doc=None, # type: str qualname=None, # type: str co_name=None, # type: str module_name=None, # type: str **attrs ): """ A decorator for functions, to change their signature. The new signature should be compliant with the old one. ```python @with_signature(<arguments>) def impl(...): ... ``` is totally equivalent to `impl = create_function(<arguments>, func_impl=impl)` except for one additional behaviour: - If `func_signature` is set to `None`, there is no `TypeError` as in create_function. Instead, this simply applies the new metadata (name, doc, module_name, attrs) to the decorated function without creating a wrapper. `add_source`, `add_impl` and `inject_as_first_arg` should not be set in this case. :param func_signature: the new signature of the decorated function. Either a string without 'def' such as "foo(a, b: int, *args, **kwargs)" of "(a, b: int)", or a `Signature` object, for example from the output of `inspect.signature` or from the `funcsigs.signature` backport. Note that these objects can be created manually too. If the signature is provided as a string and contains a non-empty name, this name will be used instead of the one of the decorated function. Finally `None` can be provided to indicate that user wants to only change the medatadata (func_name, doc, module_name, attrs) of the decorated function, without generating a new function. :param inject_as_first_arg: if `True`, the created function will be injected as the first positional argument of the decorated function. This can be handy in case the implementation is shared between several facades and needs to know from which context it was called. Default=`False` :param func_name: provide a non-`None` value to override the created function `__name__` and `__qualname__`. If this is `None` (default), the `__name__` will default to the ones of the decorated function if `func_signature` is a `Signature`, or to the name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name. :param add_source: a boolean indicating if a '__source__' annotation should be added to the generated function (default: True) :param add_impl: a boolean indicating if a '__func_impl__' annotation should be added to the generated function (default: True) :param doc: a string representing the docstring that will be used to set the __doc__ attribute on the generated function. If None (default), the doc of the decorated function will be used. :param qualname: a string representing the qualified name to be used. If None (default), the `__qualname__` will default to the one of `func_impl` if `func_signature` is a `Signature`, or to the name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name. :param co_name: a string representing the name to be used in the compiled code of the function. If None (default), the `__code__.co_name` will default to the one of `func_impl` if `func_signature` is a `Signature`, or to the name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name. :param module_name: the name of the module to be set on the function (under __module__ ). If None (default), the `__module__` attribute of the decorated function will be used. :param attrs: other keyword attributes that should be set on the function. Note that the full `__dict__` of the decorated function is not automatically copied. """ if func_signature is None and co_name is None: # make sure that user does not provide non-default other args if inject_as_first_arg or not add_source or not add_impl: raise ValueError("If `func_signature=None` no new signature will be generated so only `func_name`, " "`module_name`, `doc` and `attrs` should be provided, to modify the metadata.") else: def replace_f(f): # manually apply all the non-None metadata, but do not call create_function - that's useless if func_name is not None: f.__name__ = func_name if doc is not None: f.__doc__ = doc if qualname is not None: f.__qualname__ = qualname if module_name is not None: f.__module__ = module_name for k, v in attrs.items(): setattr(f, k, v) return f else: def replace_f(f): return create_function(func_signature=func_signature, func_impl=f, func_name=func_name, inject_as_first_arg=inject_as_first_arg, add_source=add_source, add_impl=add_impl, doc=doc, qualname=qualname, co_name=co_name, module_name=module_name, _with_sig_=True, # special trick to tell create_function that we're @with_signature **attrs ) return replace_f def remove_signature_parameters(s, *param_names): """ Removes the provided parameters from the signature `s` (returns a new `Signature` instance). :param s: :param param_names: a list of parameter names to remove :return: """ params = OrderedDict(s.parameters.items()) for param_name in param_names: del params[param_name] return s.replace(parameters=params.values()) def add_signature_parameters(s, # type: Signature first=(), # type: Union[str, Parameter, Iterable[Union[str, Parameter]]] last=(), # type: Union[str, Parameter, Iterable[Union[str, Parameter]]] custom=(), # type: Union[Parameter, Iterable[Parameter]] custom_idx=-1 # type: int ): """ Adds the provided parameters to the signature `s` (returns a new `Signature` instance). :param s: the original signature to edit :param first: a single element or a list of `Parameter` instances to be added at the beginning of the parameter's list. Strings can also be provided, in which case the parameter kind will be created based on best guess. :param last: a single element or a list of `Parameter` instances to be added at the end of the parameter's list. Strings can also be provided, in which case the parameter kind will be created based on best guess. :param custom: a single element or a list of `Parameter` instances to be added at a custom position in the list. That position is determined with `custom_idx` :param custom_idx: the custom position to insert the `custom` parameters to. :return: a new signature created from the original one by adding the specified parameters. """ params = OrderedDict(s.parameters.items()) lst = list(params.values()) # insert at custom position (but keep the order, that's why we use 'reversed') try: for param in reversed(custom): if param.name in params: raise ValueError("Parameter with name '%s' is present twice in the signature to create" % param.name) else: lst.insert(custom_idx, param) except TypeError: # a single argument if custom.name in params: raise ValueError("Parameter with name '%s' is present twice in the signature to create" % custom.name) else: lst.insert(custom_idx, custom) # prepend but keep the order first_param_kind = None try: for param in reversed(first): if isinstance(param, string_types): # Create a Parameter with auto-guessed 'kind' if first_param_kind is None: # by default use this first_param_kind = Parameter.POSITIONAL_OR_KEYWORD try: # check the first parameter kind first_param_kind = next(iter(params.values())).kind except StopIteration: # no parameter - ok pass # if the first parameter is a pos-only or a varpos we have to change to pos only. if first_param_kind in (Parameter.POSITIONAL_ONLY, Parameter.VAR_POSITIONAL): first_param_kind = Parameter.POSITIONAL_ONLY param = Parameter(name=param, kind=first_param_kind) else: # remember the kind first_param_kind = param.kind if param.name in params: raise ValueError("Parameter with name '%s' is present twice in the signature to create" % param.name) else: lst.insert(0, param) except TypeError: # a single argument if first.name in params: raise ValueError("Parameter with name '%s' is present twice in the signature to create" % first.name) else: lst.insert(0, first) # append last_param_kind = None try: for param in last: if isinstance(param, string_types): # Create a Parameter with auto-guessed 'kind' if last_param_kind is None: # by default use this last_param_kind = Parameter.POSITIONAL_OR_KEYWORD try: # check the last parameter kind last_param_kind = next(reversed(params.values())).kind except StopIteration: # no parameter - ok pass # if the last parameter is a keyword-only or a varkw we have to change to kw only. if last_param_kind in (Parameter.KEYWORD_ONLY, Parameter.VAR_KEYWORD): last_param_kind = Parameter.KEYWORD_ONLY param = Parameter(name=param, kind=last_param_kind) else: # remember the kind last_param_kind = param.kind if param.name in params: raise ValueError("Parameter with name '%s' is present twice in the signature to create" % param.name) else: lst.append(param) except TypeError: # a single argument if last.name in params: raise ValueError("Parameter with name '%s' is present twice in the signature to create" % last.name) else: lst.append(last) return s.replace(parameters=lst) def with_partial(*preset_pos_args, **preset_kwargs): """ Decorator to 'partialize' a function using `partial` :param preset_pos_args: :param preset_kwargs: :return: """ def apply_decorator(f): return partial(f, *preset_pos_args, **preset_kwargs) return apply_decorator def partial(f, # type: Callable *preset_pos_args, # type: Any **preset_kwargs # type: Any ): """ Equivalent of `functools.partial` but relies on a dynamically-created function. As a result the function looks nicer to users in terms of apparent documentation, name, etc. See [documentation](./index.md#removing-parameters-easily) for details. :param preset_pos_args: :param preset_kwargs: :return: """ # TODO do we need to mimic `partial`'s behaviour concerning positional args? # (1) remove/change all preset arguments from the signature orig_sig = signature(f) if preset_pos_args or preset_kwargs: new_sig = gen_partial_sig(orig_sig, preset_pos_args, preset_kwargs, f) else: new_sig = None if _is_generator_func(f): if sys.version_info >= (3, 3): from makefun._main_py35_and_higher import make_partial_using_yield_from partial_f = make_partial_using_yield_from(new_sig, f, *preset_pos_args, **preset_kwargs) else: from makefun._main_legacy_py import make_partial_using_yield partial_f = make_partial_using_yield(new_sig, f, *preset_pos_args, **preset_kwargs) elif isasyncgenfunction(f) and sys.version_info >= (3, 6): from makefun._main_py36_and_higher import make_partial_using_async_for_in_yield partial_f = make_partial_using_async_for_in_yield(new_sig, f, *preset_pos_args, **preset_kwargs) else: @wraps(f, new_sig=new_sig) def partial_f(*args, **kwargs): # since the signature does the checking for us, no need to check for redundancy. kwargs.update(preset_kwargs) return f(*itertools.chain(preset_pos_args, args), **kwargs) # update the doc. # Note that partial_f is generated above with a proper __name__ and __doc__ identical to the wrapped ones if new_sig is not None: partial_f.__doc__ = gen_partial_doc(partial_f.__name__, partial_f.__doc__, orig_sig, new_sig, preset_pos_args) # Set the func attribute as `functools.partial` does partial_f.func = f return partial_f if PY2: # In python 2 keyword-only arguments do not exist. # so if they do not have a default value, we set them with a default value # that is this singleton. This is the only way we can have the same behaviour # in python 2 in terms of order of arguments, than what funcools.partial does. class KwOnly: def __str__(self): return repr(self) def __repr__(self): return "KW_ONLY_ARG!" KW_ONLY = KwOnly() else: KW_ONLY = None def gen_partial_sig(orig_sig, # type: Signature preset_pos_args, # type: Tuple[Any] preset_kwargs, # type: Mapping[str, Any] f, # type: Callable ): """ Returns the signature of partial(f, *preset_pos_args, **preset_kwargs) Raises explicit errors in case of non-matching argument names. By default the behaviour is the same as `functools.partial`: - partialized positional arguments disappear from the signature - partialized keyword arguments remain in the signature in the same order, but all keyword arguments after them in the parameters order become keyword-only (if python 2, they do not become keyword-only as this is not allowed in the compiler, but we pass them a bad default value "KEYWORD_ONLY") :param orig_sig: :param preset_pos_args: :param preset_kwargs: :param f: used in error messages only :return: """ preset_kwargs = copy(preset_kwargs) # remove the first n positional, and assign/change default values for the keyword if len(orig_sig.parameters) < len(preset_pos_args): raise ValueError("Cannot preset %s positional args, function %s has only %s args." "" % (len(preset_pos_args), getattr(f, '__name__', f), len(orig_sig.parameters))) # then the keywords. If they have a new value override it new_params = [] kwonly_flag = False for i, (p_name, p) in enumerate(orig_sig.parameters.items()): if i < len(preset_pos_args): # preset positional arg: disappears from signature continue try: # is this parameter overridden in `preset_kwargs` ? overridden_p_default = preset_kwargs.pop(p_name) except KeyError: # no: it will appear "as is" in the signature, in the same order # However we need to change the kind if the kind is not already "keyword only" # positional only: Parameter.POSITIONAL_ONLY, VAR_POSITIONAL # both: POSITIONAL_OR_KEYWORD # keyword only: KEYWORD_ONLY, VAR_KEYWORD if kwonly_flag and p.kind not in (Parameter.VAR_KEYWORD, Parameter.KEYWORD_ONLY): if PY2: # Special : we can not make if Keyword-only, but we can not leave it without default value new_kind = p.kind # set a default value of new_default = p.default if p.default is not Parameter.empty else KW_ONLY else: new_kind = Parameter.KEYWORD_ONLY new_default = p.default p = Parameter(name=p.name, kind=new_kind, default=new_default, annotation=p.annotation) else: # yes: override definition with the default. Note that the parameter will remain in the signature # but as "keyword only" (and so will be all following args) if p.kind is Parameter.POSITIONAL_ONLY: raise NotImplementedError("Predefining a positional-only argument using keyword is not supported as in " "python 3.8.8, 'signature()' does not support such functions and raises a" "ValueError. Please report this issue if support needs to be added in the " "future.") if not PY2 and p.kind not in (Parameter.VAR_KEYWORD, Parameter.KEYWORD_ONLY): # change kind to keyword-only new_kind = Parameter.KEYWORD_ONLY else: new_kind = p.kind p = Parameter(name=p.name, kind=new_kind, default=overridden_p_default, annotation=p.annotation) # from now on, all other parameters need to be keyword-only kwonly_flag = True # preserve order new_params.append(p) new_sig = Signature(parameters=tuple(new_params), return_annotation=orig_sig.return_annotation) if len(preset_kwargs) > 0: raise ValueError("Cannot preset keyword argument(s), not present in the signature of %s: %s" "" % (getattr(f, '__name__', f), preset_kwargs)) return new_sig def gen_partial_doc(wrapped_name, wrapped_doc, orig_sig, new_sig, preset_pos_args): """ Generate a documentation indicating which positional arguments and keyword arguments are set in this partial implementation, and appending the wrapped function doc. :param wrapped_name: :param wrapped_doc: :param orig_sig: :param new_sig: :param preset_pos_args: :return: """ # generate the "equivalent signature": this is the original signature, # where all values injected by partial appear all_strs = [] kw_only = False for i, (p_name, _p) in enumerate(orig_sig.parameters.items()): if i < len(preset_pos_args): # use the preset positional. Use repr() instead of str() so that e.g. "yes" appears with quotes all_strs.append(repr(preset_pos_args[i])) else: # use the one in the new signature pnew = new_sig.parameters[p_name] if not kw_only: if (PY2 and pnew.default is KW_ONLY) or pnew.kind == Parameter.KEYWORD_ONLY: kw_only = True if PY2 and kw_only: all_strs.append(str(pnew).replace("=%s" % KW_ONLY, "")) else: all_strs.append(str(pnew)) argstring = ", ".join(all_strs) # Write the final docstring if wrapped_doc is None or len(wrapped_doc) == 0: partial_doc = "<This function is equivalent to '%s(%s)'.>\n" % (wrapped_name, argstring) else: new_line = "<This function is equivalent to '%s(%s)', see original '%s' doc below.>\n" \ "" % (wrapped_name, argstring, wrapped_name) partial_doc = new_line + wrapped_doc return partial_doc class UnsupportedForCompilation(TypeError): """ Exception raised by @compile_fun when decorated target is not supported """ pass class UndefinedSymbolError(NameError): """ Exception raised by @compile_fun when the function requires a name not yet defined """ pass class SourceUnavailable(OSError): """ Exception raised by @compile_fun when the function source is not available (inspect.getsource raises an error) """ pass def compile_fun(recurse=True, # type: Union[bool, Callable] except_names=(), # type: Iterable[str] ): """ A draft decorator to `compile` any existing function so that users cant debug through it. It can be handy to mask some code from your users for convenience (note that this does not provide any obfuscation, people can still reverse engineer your code easily. Actually the source code even gets copied in the function's `__source__` attribute for convenience): ```python from makefun import compile_fun @compile_fun def foo(a, b): return a + b assert foo(5, -5.0) == 0 print(foo.__source__) ``` yields ``` @compile_fun def foo(a, b): return a + b ``` If the function closure includes functions, they are recursively replaced with compiled versions too (only for this closure, this does not modify them otherwise). **IMPORTANT** this decorator is a "goodie" in early stage and has not been extensively tested. Feel free to contribute ! Note that according to [this post](https://stackoverflow.com/a/471227/7262247) compiling does not make the code run any faster. Known issues: `NameError` will appear if your function code depends on symbols that have not yet been defined. Make sure all symbols exist first ! See https://github.com/smarie/python-makefun/issues/47 :param recurse: a boolean (default `True`) indicating if referenced symbols should be compiled too :param except_names: an optional list of symbols to exclude from compilation when `recurse=True` :return: """ if callable(recurse): # called with no-args, apply immediately target = recurse # noinspection PyTypeChecker return compile_fun_manually(target, _evaldict=True) else: # called with parenthesis, return a decorator def apply_compile_fun(target): return compile_fun_manually(target, recurse=recurse, except_names=except_names, _evaldict=True) return apply_compile_fun def compile_fun_manually(target, recurse=True, # type: Union[bool, Callable] except_names=(), # type: Iterable[str] _evaldict=None # type: Union[bool, Dict] ): """ :param target: :return: """ if not isinstance(target, FunctionType): raise UnsupportedForCompilation("Only functions can be compiled by this decorator") if _evaldict is None or _evaldict is True: if _evaldict is True: frame = _get_callerframe(offset=1) else: frame = _get_callerframe() _evaldict, _ = extract_module_and_evaldict(frame) # first make sure that source code is available for compilation try: lines = getsource(target) except (OSError, IOError) as e: # noqa # distinct exceptions in old python versions if 'could not get source code' in str(e): raise SourceUnavailable(target, e) else: raise # compile all references first try: # python 3 func_closure = target.__closure__ func_code = target.__code__ except AttributeError: # python 2 func_closure = target.func_closure func_code = target.func_code # Does not work: if `self.i` is used in the code, `i` will appear here # if func_code is not None: # for name in func_code.co_names: # try: # eval(name, _evaldict) # except NameError: # raise UndefinedSymbolError("Symbol `%s` does not seem to be defined yet. Make sure you apply " # "`compile_fun` *after* all required symbols have been defined." % name) if recurse and func_closure is not None: # recurse-compile for name, cell in zip(func_code.co_freevars, func_closure): if name in except_names: continue if name not in _evaldict: raise UndefinedSymbolError("Symbol %s does not seem to be defined yet. Make sure you apply " "`compile_fun` *after* all required symbols have been defined." % name) try: value = cell.cell_contents except ValueError: # empty cell continue else: # non-empty cell try: # note : not sure the compilation will be made in the appropriate order of dependencies... # if not, users will have to do it manually _evaldict[name] = compile_fun_manually(value, recurse=recurse, except_names=except_names, _evaldict=_evaldict) except (UnsupportedForCompilation, SourceUnavailable): pass # now compile from sources lines = dedent(lines) source_lines = lines if lines.startswith('@compile_fun'): lines = '\n'.join(lines.splitlines()[1:]) if '@compile_fun' in lines: raise ValueError("@compile_fun seems to appear several times in the function source") if lines[-1] != '\n': lines += '\n' # print("compiling: ") # print(lines) new_f = _make(target.__name__, (), lines, _evaldict) new_f.__source__ = source_lines return new_f ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/src/makefun/py.typed����������������������������������������������������������0000664�0000000�0000000�00000000000�14644215064�0020457�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/tests/������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14644215064�0015717�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/tests/__init__.py�������������������������������������������������������������0000664�0000000�0000000�00000000000�14644215064�0020016�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/tests/_issue_85_module.py�����������������������������������������������������0000664�0000000�0000000�00000000263�14644215064�0021442�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������def forwardref_method(foo: "ForwardRef", bar: str) -> "ForwardRef": return ForwardRef(foo.x + bar) class ForwardRef: def __init__(self, x="default"): self.x = x ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/tests/_test_py35.py�����������������������������������������������������������0000664�0000000�0000000�00000001453�14644215064�0020272�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from asyncio import sleep def make_native_coroutine_handler(): """Returns a native coroutine to be used in tests""" async def my_native_coroutine_handler(sleep_time): await sleep(sleep_time) return sleep_time return my_native_coroutine_handler def make_ref_function(): """Returns a function with a type hint that is locally defined """ # the symbol is defined here, so it is not seen outside class A: pass def ref(a: A) -> A: pass return ref def make_ref_function2(): """ """ from typing import Any def ref(a: Any): pass return ref def get_my_wrapper(f): def my_wrapper(*args, a, **kwargs): # a is automatically extracted from the sig return a + f(*args, **kwargs) return my_wrapper ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/tests/_test_py36.py�����������������������������������������������������������0000664�0000000�0000000�00000000630�14644215064�0020267�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������def make_async_generator(): """Returns a new async generator function to use in tests.""" async def f(v): yield v return f def make_async_generator_wrapper(async_gen_f): """Returns a new async generator function wrapping `f`, to use in tests.""" async def wrapper(*args, **kwargs): async for v in async_gen_f(*args, **kwargs): yield v return wrapper ��������������������������������������������������������������������������������������������������������python-makefun-1.15.4/tests/_test_py38.py�����������������������������������������������������������0000664�0000000�0000000�00000000154�14644215064�0020272�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������def make_pos_only_f(): def f(a, b, c, /, *, d, **e): return a + b + c + d + sum(e) return f ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/tests/test_advanced.py��������������������������������������������������������0000664�0000000�0000000�00000017421�14644215064�0021102�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import logging import sys import pytest from makefun.main import get_signature_from_string, with_signature from makefun import create_wrapper, wraps try: # python 3.3+ from inspect import signature, Signature, Parameter except ImportError: from funcsigs import signature, Signature, Parameter def test_non_representable_defaults(): """ Tests that non-representable default values are handled correctly """ def foo(logger=logging.getLogger('default')): pass @wraps(foo) def bar(*args, **kwargs): pass bar() def test_preserve_attributes(): """ Tests that attributes are preserved """ def foo(): pass setattr(foo, 'a', True) @wraps(foo) def bar(*args, **kwargs): pass assert bar.a def test_empty_name_in_string(): """ Tests that string signatures can now be provided without function name""" if sys.version_info < (3, 0): str_sig = '(a)' else: str_sig = '(a:int)' func_name, func_sig, func_sig_str = get_signature_from_string(str_sig, locals()) assert func_name is None # to handle type hints in signatures in python 3.5 we have to always remove the spaces assert str(func_sig).replace(' ', '') == str_sig assert func_sig_str == str_sig + ':' def test_same_than_wraps_basic(): """Tests that the metadata set by @wraps is correct""" from tests.test_doc import test_from_sig_wrapper from functools import wraps as functools_wraps def foo_wrapper(*args, **kwargs): """ hoho """ pass functool_wrapped = functools_wraps(test_from_sig_wrapper)(foo_wrapper) # WARNING: functools.wraps irremediably contaminates foo_wrapper, we have to redefine it def foo_wrapper(*args, **kwargs): """ hoho """ pass makefun_wrapped = wraps(test_from_sig_wrapper)(foo_wrapper) # compare with the default behaviour of with_signature, that is to copy metadata from the decorated makefun_with_signature_inverted = with_signature(signature(test_from_sig_wrapper))(test_from_sig_wrapper) makefun_with_signature_normal = with_signature(signature(test_from_sig_wrapper))(foo_wrapper) for field in ('__module__', '__name__', '__qualname__', '__doc__', '__annotations__'): if sys.version_info < (3, 0) and field in {'__qualname__', '__annotations__'}: pass else: assert getattr(functool_wrapped, field) == getattr(makefun_wrapped, field), "field %s is different" % field assert getattr(functool_wrapped, field) == getattr(makefun_with_signature_inverted, field), "field %s is different" % field if field != '__annotations__': assert getattr(functool_wrapped, field) != getattr(makefun_with_signature_normal, field), "field %s is identical" % field def tests_wraps_sigchange(): """ Tests that wraps can be used to change the signature """ def foo(a): """ hoho """ return a @wraps(foo, new_sig="(a, b=0)") def goo(*args, **kwargs): kwargs.pop('b') return foo(*args, **kwargs) for field in ('__module__', '__name__', '__qualname__', '__doc__', '__annotations__'): if sys.version_info < (3, 0) and field in {'__qualname__', '__annotations__'}: pass else: assert getattr(goo, field) == getattr(foo, field), "field %s is different" % field assert str(signature(goo)) == "(a, b=0)" assert goo('hello') == 'hello' def tests_wraps_lambda(): """ Tests that `@wraps` can duplicate the signature of a lambda """ foo = lambda a: a @wraps(foo) def goo(*args, **kwargs): return foo(*args, **kwargs) assert goo.__name__ == (lambda: None).__name__ assert str(signature(goo)) == '(a)' assert goo('hello') == 'hello' def tests_wraps_renamed_lambda(): """ Tests that `@wraps` can duplicate the signature of a lambda that has been renamed """ foo = lambda a: a foo.__name__ = 'bar' @wraps(foo) def goo(*args, **kwargs): return foo(*args, **kwargs) assert goo.__name__ == 'bar' assert str(signature(goo)) == '(a)' assert goo('hello') == 'hello' def test_lambda_signature_str(): """ Tests that `@with_signature` can create a lambda from a signature string """ new_sig = '(a, b=5)' @with_signature(new_sig, func_name='<lambda>') def foo(a, b): return a + b assert foo.__name__ == '<lambda>' assert foo.__code__.co_name == '<lambda>' assert str(signature(foo)) == new_sig assert foo(a=4) == 9 def test_co_name(): """ Tests that `@with_signature` can be used to change the __code__.co_name """ @with_signature('()', co_name='bar') def foo(): return 'hello' assert foo.__name__ == 'foo' assert foo.__code__.co_name == 'bar' assert foo() == 'hello' def test_with_signature_lambda(): """ Tests that `@with_signature` can be used to change the __code__.co_name to `'<lambda>'` """ @with_signature('()', co_name='<lambda>') def foo(): return 'hello' assert foo.__code__.co_name == '<lambda>' assert foo() == 'hello' def test_create_wrapper_lambda(): """ Tests that `create_wrapper` returns a lambda function when given a lambda function to wrap""" def foo(): return 'hello' bar = create_wrapper(lambda: None, foo) assert bar.__name__ == '<lambda>' assert bar() == 'hello' def test_invalid_co_name(): """ Tests that `@with_signature` raises a `ValueError` when given an `co_name` that cannot be duplicated. """ with pytest.raises(ValueError): @with_signature('()', co_name='<invalid>') def foo(): return 'hello' def test_invalid_func_name(): """ Tests that `@with_signature` can duplicate a func_name that is invalid in a function definition. """ @with_signature('()', func_name='<invalid>') def foo(): return 'hello' assert foo.__name__ == '<invalid>' assert foo() == 'hello' @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires python3 or higher") def test_qualname_when_nested(): """ Tests that qualname is correctly set when `@with_signature` is applied on nested functions """ class C: def f(self): pass class D: @with_signature("(self, a)") def g(self): pass assert C.__qualname__ == 'test_qualname_when_nested.<locals>.C' assert C.f.__qualname__ == 'test_qualname_when_nested.<locals>.C.f' assert C.D.__qualname__ == 'test_qualname_when_nested.<locals>.C.D' # our mod assert C.D.g.__qualname__ == 'test_qualname_when_nested.<locals>.C.D.g' assert str(signature(C.D.g)) == "(self, a)" @pytest.mark.skipif(sys.version_info < (3, 5), reason="requires python 3.5 or higher (non-comment type hints)") def test_type_hint_error(): """ Test for https://github.com/smarie/python-makefun/issues/32 """ from tests._test_py35 import make_ref_function ref_f = make_ref_function() @wraps(ref_f) def foo(a): return a assert foo(10) == 10 @pytest.mark.skipif(sys.version_info < (3, 5), reason="requires python 3.5 or higher (non-comment type hints)") def test_type_hint_error2(): """ Test for https://github.com/smarie/python-makefun/issues/32 """ from tests._test_py35 import make_ref_function2 ref_f = make_ref_function2() @wraps(ref_f) def foo(a): return a assert foo(10) == 10 @pytest.mark.skipif(sys.version_info < (3, 5), reason="requires python 3.5 or higher (non-comment type hints)") def test_type_hint_error_sigchange(): """ Test for https://github.com/smarie/python-makefun/issues/32 """ from tests._test_py35 import make_ref_function from typing import Any ref_f = make_ref_function() @wraps(ref_f, new_sig="(a: Any)") def foo(a): return a assert foo(10) == 10 �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/tests/test_compile_deco.py����������������������������������������������������0000664�0000000�0000000�00000010046�14644215064�0021753�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Authors: Sylvain Marie <sylvain.marie@se.com> # # Copyright (c) Schneider Electric Industries, 2020. All right reserved. from textwrap import dedent import pytest from makefun import compile_fun, UnsupportedForCompilation, UndefinedSymbolError def test_compilefun(): """tests that @compile_fun works correctly""" @compile_fun def foo(a, b): return a + b res = foo(5, -5.0) assert res == 0 ref = """ @compile_fun def foo(a, b): return a + b """ assert foo.__source__ == dedent(ref[1:]) def get_code(target): try: # python 3 func_code = target.__code__ except AttributeError: # python 2 func_code = target.func_code return func_code def is_compiled(target): fname = get_code(target).co_filename return fname != __file__ and 'makefun-gen' in fname def test_compilefun_nested(): """tests that @compile_fun correctly compiles nested functions recursively""" def foo(a, b): return a + b @compile_fun def bar(a, b): assert is_compiled(foo) return foo(a, b) res = bar(5, -5.0) assert res == 0 def test_compilefun_nested_already_compiled(): """tests that @compile_fun correctly handles when a required function was already compiled""" @compile_fun def foo(a, b): return a + b @compile_fun def bar(a, b): assert is_compiled(foo) return foo(a, b) res = bar(5, -5.0) assert res == 0 @pytest.mark.parametrize("variant", ['all', 'named'], ids="variant={}".format) def test_compilefun_nested_exclude(variant): """tests that the `except_names` argument of @compile_fun works correctly""" def foo(a, b): return a + b if variant == 'all': @compile_fun(recurse=False) def bar(a, b): assert not is_compiled(foo) return foo(a, b) else: @compile_fun(except_names=('foo', )) def bar(a, b): assert not is_compiled(foo) return foo(a, b) res = bar(5, -5.0) assert res == 0 def test_compilefun_co_names(): """Test that today we do not compile imported names.""" @compile_fun def foo(): # TODO one day it would be great to selectively recurse through such imported names. Unfortunately, # this comes with *many* side effects including compilation order, appropriate propagation or # non-propagation of globals(), locals() # See https://github.com/smarie/python-makefun/issues/52 assert not is_compiled(dedent) return dedent(" hoho") res = foo() assert res == "hoho" def test_compilefun_nameerror(): """Tests that the `NameError` is raised at creation time and not at call time""" with pytest.raises(UndefinedSymbolError): @compile_fun def fun_requiring_unknown_name(a, b): return unknown_name(a, b) def unknown_name(a, b): return a + b def test_compilefun_method(): """Tests that @compilefun works for class methods""" class A: @compile_fun def meth1(self, par1): print("in A.meth1: par1 =", par1) a = A() a.meth1("via meth1") class A: def __init__(self): self.i = 1 @compile_fun def add(self, a): return self.i + a a = A() assert A().add(-1) == 0 def test_compileclass_decorator(): """tests that applying decorator on a class raises an error """ with pytest.raises(UnsupportedForCompilation): @compile_fun class A(object): pass # def test_compileclass_decorator(): # # @compile_fun # class A(object): # pass # # assert A() is not None # # @compile_fun # class A(int, object): # pass # # assert A() is not None # # @compile_fun # class A(object): # def __init__(self): # pass # # assert A() is not None # # @compile_fun # class A(int): # pass # # def compute(self): # # return self + 2 # # assert A(2) + 2 == 4 ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/tests/test_create_from_signature.py�������������������������������������������0000664�0000000�0000000�00000002201�14644215064�0023672�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import sys import pytest from makefun import create_function try: # python 3.3+ from inspect import signature, Signature, Parameter except ImportError: from funcsigs import signature, Signature, Parameter def my_handler(*args, **kwargs): """This docstring will be used in the generated function by default""" print("my_handler called !") return args, kwargs def test_positional_only(): """Tests that as of today one cannot create positional-only functions""" params = [Parameter('a', kind=Parameter.POSITIONAL_ONLY), Parameter('args', kind=Parameter.VAR_POSITIONAL), Parameter('kwargs', kind=Parameter.VAR_KEYWORD)] func_signature = Signature(parameters=params) if sys.version_info < (3, 8): with pytest.raises(SyntaxError): create_function(func_signature, my_handler, func_name="foo") else: dynamic_fun =create_function(func_signature, my_handler, func_name="foo") assert "\n" + dynamic_fun.__source__ == """ def foo(a, /, *args, **kwargs): return _func_impl_(a, *args, **kwargs) """ assert dynamic_fun(0, 1) == ((0, 1), {}) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/tests/test_create_from_string.py����������������������������������������������0000664�0000000�0000000�00000013765�14644215064�0023220�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import sys import pytest from makefun import create_function # Python 2 does not support function annotations; Python 3.0-3.4 do not support variable annotations. params_type_hints_allowed = sys.version_info.major >= 3 and sys.version_info.minor >= 5 if params_type_hints_allowed: type_hints_variants = [False, 1, 2] else: type_hints_variants = [False] @pytest.mark.parametrize('with_self_ref', [True, False], ids="self_ref={}".format) @pytest.mark.parametrize('params_type_hints', type_hints_variants, ids="type_hints={}".format) def test_basic(params_type_hints, with_self_ref): """ Tests that we can create a simple dynamic function from a signature string, redirected to a generic handler. """ if params_type_hints == 1: from typing import Any func_signature = "foo(b, # type: int\n" \ " a = 0, # type: float\n" \ " ):\n # type: (...) -> Any" elif params_type_hints == 2: from typing import Any func_signature = "foo(b: int, a: float=0) -> Any" else: func_signature = "foo(b, a=0)" # this handler will grab the inputs and return them if with_self_ref: def identity_handler(facade, *args, **kwargs): """test doc""" return facade, args, kwargs else: def identity_handler(*args, **kwargs): """test doc""" return args, kwargs # create the dynamic function dynamic_fun = create_function(func_signature, identity_handler, inject_as_first_arg=with_self_ref) # a few asserts on the signature assert dynamic_fun.__name__ == 'foo' assert dynamic_fun.__doc__ == 'test doc' assert dynamic_fun.__module__ == test_basic.__module__ if params_type_hints == 1: # unfortunately assert dynamic_fun.__annotations__ == {} elif params_type_hints == 2: assert dynamic_fun.__annotations__ == {'a': float, 'b': int, 'return': Any} else: assert dynamic_fun.__annotations__ == {} assert dynamic_fun.__defaults__ == (0,) assert dynamic_fun.__kwdefaults__ is None if params_type_hints != 1: func_signature = func_signature + ":" if with_self_ref: src = "def " + func_signature + '\n return _func_impl_(foo, b=b, a=a)\n' else: src = "def " + func_signature + '\n return _func_impl_(b=b, a=a)\n' dct = {'__source__': src, '__func_impl__': identity_handler} if not params_type_hints_allowed: dct['__annotations__'] = dict() dct['__kwdefaults__'] = None assert vars(dynamic_fun) == dct # try to call it ! if with_self_ref: f, args, kwargs = dynamic_fun(2) assert f is dynamic_fun else: args, kwargs = dynamic_fun(2) assert args == () assert kwargs == {'a': 0, 'b': 2} # def test_sig(): # b_param = Parameter() # parameters = OrderedDict( # ((b_param.name, b_param), # (a_param.name, a_param)) # ) # # s = Signature().replace(parameters=parameters) # # s = s.replace(return_annotation=return_annotation) @pytest.mark.skip("This test is known to fail because inspect.signature does not detect comment type hints") def test_type_comments(): """Tests that """ func_signature = """ foo(b, # type: int a = 0, # type: float ): # type: (...) -> str """ def dummy_handler(*args, **kwargs): return "hello" dynamic_fun = create_function(func_signature, dummy_handler) assert dynamic_fun.__annotations__ == {'a': float, 'b': int, 'return': str} params_type_hints_allowed = sys.version_info.major >= 3 and sys.version_info.minor >= 5 star_followed_by_arg_allowed = sys.version_info.major >= 3 class TestArguments: def test_case_simple(self): params_str = "b, a = 0" # case_simple.__name__ = params_str # param_names = ['b', 'a'] inputs = "12" args = () kwargs = {'a': 0, 'b': 12} _test_arguments(params_str, inputs, (args, kwargs)) @pytest.mark.skipif(not star_followed_by_arg_allowed, reason='not allowed in this version of python') def test_case_simple_with_star(self): params_str = "b, *, a = 0" # case_simple_with_star.__name__ = params_str # param_names = ['b', '*', 'a'] inputs = "12" args = () kwargs = {'a': 0, 'b': 12} _test_arguments(params_str, inputs, (args, kwargs)) @pytest.mark.skipif(not star_followed_by_arg_allowed, reason='not allowed in this version of python') def test_case_simple_with_star_args1(self): params_str = "b, *args, a = 0" # case_simple_with_star_args1.__name__ = params_str # param_names = ['b', 'a'] inputs = "12" # args = () # kwargs = {'a': 0, 'b': 12} args = (12,) kwargs = {'a': 0} _test_arguments(params_str, inputs, (args, kwargs)) @pytest.mark.skipif(not star_followed_by_arg_allowed, reason='not allowed in this version of python') def test_case_simple_with_star_args2(self): params_str = "*args, a = 0" # case_simple_with_star_args2.__name__ = params_str # param_names = ['b', 'a'] inputs = "12" args = (12, ) kwargs = {'a': 0} _test_arguments(params_str, inputs, (args, kwargs)) def test_case_with_type_comments_and_newlines(self): params_str = "b, # type: int\n" \ "a = 0 # type: float\n" # test_case_with_type_comments_and_newlines.__name__ = params_str # param_names = ['b', 'a'] inputs = "12" args = () kwargs = {'a': 0, 'b': 12} _test_arguments(params_str, inputs, (args, kwargs)) def _test_arguments(params_str, inputs, expected): """ Tests that the `PARAM_DEF` regexp works correctly """ def generic_handler(*args, **kwargs): return args, kwargs f = create_function("foo(%s)" % params_str, generic_handler) args, kwargs = eval("f(%s)" % inputs, globals(), locals()) assert (args, kwargs) == expected �����������python-makefun-1.15.4/tests/test_doc.py�������������������������������������������������������������0000664�0000000�0000000�00000026203�14644215064�0020100�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import sys from random import random import pytest try: # python 3.3+ from inspect import signature, Signature, Parameter except ImportError: from funcsigs import signature, Signature, Parameter from makefun import create_function, add_signature_parameters, remove_signature_parameters, with_signature, wraps, \ create_wrapper PY2 = sys.version_info < (3,) @pytest.mark.parametrize('decorator', [False, True], ids="decorator={}".format) @pytest.mark.parametrize('type', ['str', 'Signature'], ids="type={}".format) def test_ex_nihilo(type, decorator): """ First example from the documentation: tests that we can generate a function from a string """ # (1) define the signature. if type == 'str': func_sig = "foo(b, a=0)" func_name = None else: parameters = [Parameter('b', kind=Parameter.POSITIONAL_OR_KEYWORD), Parameter('a', kind=Parameter.POSITIONAL_OR_KEYWORD, default=0), ] func_sig = Signature(parameters) func_name = 'foo' # (2) define the function implementation def func_impl(*args, **kwargs): """This docstring will be used in the generated function by default""" print("func_impl called !") return args, kwargs # (3) create the dynamic function if decorator: gen_func = with_signature(func_sig, func_name=func_name)(func_impl) else: gen_func = create_function(func_sig, func_impl, func_name=func_name) # first check the source code ref_src = "def foo(b, a=0):\n return _func_impl_(b=b, a=a)\n" print("Generated Source :\n" + gen_func.__source__) assert gen_func.__source__ == ref_src # then the behaviour args, kwargs = gen_func(2) assert args == () assert kwargs == {'a': 0, 'b': 2} @pytest.mark.skipif(sys.version_info < (3, 0), reason="keyword-only signatures require python 3+") def test_ex_nihilo_kw_only(): """Same than ex nihilo but keyword only""" def func_impl(*args, **kwargs): """This docstring will be used in the generated function by default""" print("func_impl called !") return args, kwargs func_sig = "foo(b, *, a=0, **kwargs)" gen_func = create_function(func_sig, func_impl) ref_src = "def foo(b, *, a=0, **kwargs):\n return _func_impl_(b=b, a=a, **kwargs)\n" print(gen_func.__source__) assert gen_func.__source__ == ref_src @pytest.mark.parametrize('use_decorator', [False, True], ids="use_decorator={}".format) def test_from_sig_wrapper(use_decorator): """ Tests that we can create a function from a Signature object """ def foo(b, a=0): print("foo called: b=%s, a=%s" % (b, a)) return b, a # capture the name and signature of existing function `foo` func_name = foo.__name__ original_func_sig = signature(foo) print("Original Signature: %s" % original_func_sig) # modify the signature to add a new parameter params = list(original_func_sig.parameters.values()) params.insert(0, Parameter('z', kind=Parameter.POSITIONAL_OR_KEYWORD)) func_sig = original_func_sig.replace(parameters=params) print("New Signature: %s" % func_sig) # define the implementation def func_impl(z, *args, **kwargs): print("func_impl called ! z=%s" % z) # call the foo function output = foo(*args, **kwargs) # return augmented output return z, output # create the dynamic function if use_decorator: gen_func = wraps(foo, new_sig=func_sig)(func_impl) else: gen_func = create_wrapper(foo, func_impl, new_sig=func_sig) # check the source code ref_src = "def foo(z, b, a=0):\n return _func_impl_(z=z, b=b, a=a)\n" print("Generated Source :\n" + gen_func.__source__) assert gen_func.__source__ == ref_src # then the behaviour assert gen_func(3, 2) == (3, (2, 0)) def test_helper_functions(): """ Tests that the signature modification helpers work """ def foo(b, c, a=0): pass # original signature foo_sig = signature(foo) print("original signature: %s" % foo_sig) # let's modify it new_sig = add_signature_parameters(foo_sig, first=Parameter('z', kind=Parameter.POSITIONAL_OR_KEYWORD), last=Parameter('o', kind=Parameter.POSITIONAL_OR_KEYWORD, default=True) ) new_sig = remove_signature_parameters(new_sig, 'b', 'a') print("modified signature: %s" % new_sig) assert str(new_sig) == '(z, c, o=True)' def test_injection(): """ Tests that the function can be injected as first argument when inject_as_first_arg=True """ def generic_handler(f, *args, **kwargs): print("This is generic handler called by %s" % f.__name__) # here you could use f.__name__ in a if statement to determine what to do if f.__name__ == "func1": print("called from func1 !") return args, kwargs # generate 2 functions func1 = create_function("func1(a, b)", generic_handler, inject_as_first_arg=True) func2 = create_function("func2(a, d)", generic_handler, inject_as_first_arg=True) func1(1, 2) func2(1, 2) def test_var_length(): """Demonstrates how variable-length arguments are passed to the handler """ # define the handler that should be called def generate_function(func_sig, dummy_call): def func_impl(*args, **kwargs): """This docstring will be used in the generated function by default""" print("func_impl called !") dummy_call(*args, **kwargs) return args, kwargs return create_function(func_sig, func_impl) func_sig = "foo(a, b=0, *args, **kwargs)" def dummy_call(a, b=0, *args, **kwargs): print() gen_func = generate_function(func_sig, dummy_call) print(gen_func.__source__) # unfortunately we can not have this because as soon as users provide a bit more positional args they there # are TypeErrors "got multiple values for argument 'a'" # assert gen_func(0, 1, 2) == ((2), {'a': 0, 'b': 1}) assert gen_func(0, 1, 2) == ((0, 1, 2), {}) assert gen_func(0, b=1) == ((0, 1), {}) # checks that the order is correctly set assert gen_func(b=1, a=0) == ((0, 1), {}) with pytest.raises(TypeError): gen_func(2, a=0, b=1) # -- func_sig = "foo(b=0, *args, **kwargs)" def dummy_call(b=0, *args, **kwargs): print() gen_func = generate_function(func_sig, dummy_call) print(gen_func.__source__) assert gen_func(1, 0) == ((1, 0), {}) assert gen_func(b=1) == ((1, ), {}) with pytest.raises(TypeError): gen_func(1, b=0) def test_positional_only(): """Tests that as of today positional-only signatures translate to bad strings """ params = [Parameter('a', kind=Parameter.POSITIONAL_ONLY), Parameter('b', kind=Parameter.POSITIONAL_OR_KEYWORD)] assert str(Signature(parameters=params)) in {"(<a>, b)", "(a, /, b)"} def test_with_signature(): """ Tests that @with_signature works as expected """ @with_signature("foo(a)") def foo(**kwargs): return 'hello' with pytest.raises(TypeError): foo() assert str(signature(foo)) == "(a)" assert foo('dummy') == 'hello' def test_with_signature_none(): """""" def foo(a): return a new = with_signature(None, func_name='f')(foo) assert new('hello') == 'hello' assert str(signature(new)) == "(a)" # check that the object was not wrapped assert new == foo assert new.__name__ == 'f' def test_wraps(capsys): """ """ # we want to wrap this function f to add some prints before calls def foo(a, b=1): return a + b # create our wrapper: it will have the same signature than f @wraps(foo) def enhanced_foo(*args, **kwargs): # we can very reliably access the value for 'b' print('hello!') print('b=%s' % kwargs['b']) # then call f as usual return foo(*args, **kwargs) assert enhanced_foo(1, 2) == 3 assert enhanced_foo(b=0, a=1) == 1 assert enhanced_foo(1) == 2 with pytest.raises(TypeError): # does not print anything in case of error enhanced_foo() captured = capsys.readouterr() with capsys.disabled(): print(captured.out) assert captured.out == """hello! b=2 hello! b=0 hello! b=1 """ def test_wraps_functools(capsys): """ same with functools.wraps """ from functools import wraps # we want to wrap this function f to add some prints before calls def foo(a, b=1): return a + b # create our wrapper: it will have the same signature than f @wraps(foo) def enhanced_foo(*args, **kwargs): # we can very reliably access the value for 'b' print('hello!') print('b=%s' % kwargs['b']) # then call f as usual return foo(*args, **kwargs) # assert enhanced_foo(1, 2) == 3 assert enhanced_foo(b=0, a=1) == 1 # assert enhanced_foo(1) == 2 with pytest.raises(KeyError): # prints a message in case of error enhanced_foo() captured = capsys.readouterr() with capsys.disabled(): print(captured.out) assert captured.out == """hello! b=0 hello! """ def test_wraps_remove(): def inject_random_a(f): """ A decorator that injects a random number inside the `a` argument, removing it from the exposed signature """ @wraps(f, remove_args='a') def my_wrapper(*args, **kwargs): # generate a random value for a and inject it in the args for f kwargs['a'] = random() return f(*args, **kwargs) return my_wrapper @inject_random_a def summer(a, b): return a + b assert 12 <= summer(b=12) <= 13 def test_wraps_add_doc(): from makefun import wraps def foo(b, a=0): print("foo called: b=%s, a=%s" % (b, a)) return b, a @wraps(foo, prepend_args='z') def foo_wrapper(z, *args, **kwargs): print("foo_wrapper called ! z=%s" % z) # call the foo function output = foo(*args, **kwargs) # return augmented output return z, output # call it assert foo_wrapper(3, 2) == (3, (2, 0)) @pytest.mark.parametrize("prepend", [True, False], ids="prepend={}".format) def test_wraps_add(prepend): def add_a_to_result(f): """ A decorator that injects a random number inside the `a` argument, removing it from the exposed signature """ if prepend: decorator = wraps(f, prepend_args='a') else: decorator = wraps(f, append_args='a') if PY2: def my_wrapper(*args, **kwargs): a = kwargs.pop('a') return a + f(*args, **kwargs) else: from ._test_py35 import get_my_wrapper my_wrapper = get_my_wrapper(f) my_wrapper = decorator(my_wrapper) return my_wrapper @add_a_to_result def identity(b): return b assert identity(b=12, a=0.5) == 12.5 ref_str = "(a, b)" if prepend else "(b, a)" assert str(signature(identity)) == ref_str ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/tests/test_generators_coroutines.py�������������������������������������������0000664�0000000�0000000�00000007652�14644215064�0023765�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import sys import pytest from makefun import with_signature, create_function try: from inspect import iscoroutinefunction except ImportError: # let's assume there are no coroutine functions in old Python def iscoroutinefunction(f): return False try: from inspect import isgeneratorfunction except ImportError: # assume no generator function in old Python versions def isgeneratorfunction(f): return False def test_generator(): """ Tests that we can use a generator as function_handler in `create_function`""" # define the handler that should be called def my_generator_handler(b, a=0): for i in range(a, b): yield i * i # create the dynamic function dynamic_fun = create_function("foo(a, b)", my_generator_handler) assert isgeneratorfunction(dynamic_fun) assert list(dynamic_fun(1, 4)) == [1, 4, 9] def test_generator_with_signature(): """ Tests that we can write a generator and change its signature: it will still be a generator """ @with_signature("foo(a)") def foo(*args, **kwargs): for i in range(1, 4): yield i * i assert isgeneratorfunction(foo) with pytest.raises(TypeError): foo() assert list(foo('dummy')) == [1, 4, 9] def test_generator_based_coroutine(): """ Tests that we can use a generator coroutine as function_handler in `create_function`""" # define the handler that should be called def my_gencoroutine_handler(first_msg): second_msg = (yield first_msg) yield second_msg # create the dynamic function dynamic_fun = create_function("foo(first_msg='hello')", my_gencoroutine_handler) # a legacy (generator-based) coroutine is not an asyncio coroutine.. assert not iscoroutinefunction(dynamic_fun) assert isgeneratorfunction(dynamic_fun) cor = dynamic_fun('hi') first_result = next(cor) assert first_result == 'hi' second_result = cor.send('chaps') assert second_result == 'chaps' with pytest.raises(StopIteration): cor.send('ola') @pytest.mark.skipif(sys.version_info < (3, 5), reason="native coroutines with async/await require python3.6 or higher") def test_native_coroutine(): """ Tests that we can use a native async coroutine as function_handler in `create_function`""" # define the handler that should be called from tests._test_py35 import make_native_coroutine_handler my_native_coroutine_handler = make_native_coroutine_handler() # create the dynamic function dynamic_fun = create_function("foo(sleep_time=2)", my_native_coroutine_handler) # check that this is a coroutine for inspect and for asyncio assert iscoroutinefunction(dynamic_fun) from asyncio import iscoroutinefunction as is_native_co assert is_native_co(dynamic_fun) # verify that the new function is a native coroutine and behaves correctly from asyncio import get_event_loop out = get_event_loop().run_until_complete(dynamic_fun(0.1)) assert out == 0.1 @pytest.mark.skipif(sys.version_info < (3, 5), reason="native coroutines with async/await require python3.6 or higher") def test_issue_96(): """Same as `test_native_coroutine` but tests that we can use 'return' in the coroutine name""" # define the handler that should be called from tests._test_py35 import make_native_coroutine_handler my_native_coroutine_handler = make_native_coroutine_handler() # create the dynamic function dynamic_fun = create_function("foo_returns_bar(sleep_time=2)", my_native_coroutine_handler) # check that this is a coroutine for inspect and for asyncio assert iscoroutinefunction(dynamic_fun) from asyncio import iscoroutinefunction as is_native_co assert is_native_co(dynamic_fun) # verify that the new function is a native coroutine and behaves correctly from asyncio import get_event_loop out = get_event_loop().run_until_complete(dynamic_fun(0.1)) assert out == 0.1 ��������������������������������������������������������������������������������������python-makefun-1.15.4/tests/test_issues.py����������������������������������������������������������0000664�0000000�0000000�00000016031�14644215064�0020644�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import inspect import sys import pytest from makefun.main import is_identifier try: # python 3.3+ from inspect import signature, Signature, Parameter except ImportError: from funcsigs import signature, Signature, Parameter from makefun import wraps, with_signature, partial, create_function def test_wraps_varpositional_issue_34(): """ test for https://github.com/smarie/python-makefun/issues/34 """ def f(a, *args): return a, args @wraps(f) def foo(*args, **kwargs): return f(*args, **kwargs) assert foo('hello', 12) == ("hello", (12,)) def test_varpositional2(): """ test for https://github.com/smarie/python-makefun/issues/38 """ @with_signature("(a, *args)") def foo(a, *args): assert a == 'hello' assert args == (12, ) foo('hello', 12) def test_invalid_signature_str(): """Test for https://github.com/smarie/python-makefun/issues/36""" sig = "(a):" @with_signature(sig) def foo(a): pass @pytest.mark.skipif(sys.version_info < (3, 0), reason="type hints are not allowed with this syntax in python 2") def test_invalid_signature_str_py3(): """Test for https://github.com/smarie/python-makefun/issues/36""" sig = "(a) -> int:" @with_signature(sig) def foo(a): pass def test_return_annotation_in_py2(): """Test for https://github.com/smarie/python-makefun/issues/39""" def f(): pass f.__annotations__ = {'return': None} @wraps(f) def b(): pass b() def test_init_replaced(): class Foo(object): @with_signature("(self, a)") def __init__(self, *args, **kwargs): pass f = Foo(1) class Bar(Foo): def __init__(self, *args, **kwargs): super(Bar, self).__init__(*args, **kwargs) b = Bar(2) def test_issue_55(): """Tests that no syntax error appears when no arguments are provided in the signature (name change scenario)""" # full name change including stack trace @with_signature('bar()') def foo(): return 'a' assert "bar at" in repr(foo) assert foo.__name__ == 'bar' assert foo() == 'a' # only metadata change @with_signature(None, func_name='bar') def foo(): return 'a' if sys.version_info >= (3, 0): assert "foo at" in repr(foo) assert foo.__name__ == 'bar' assert foo() == 'a' def test_partial_noargs(): """ Fixes https://github.com/smarie/python-makefun/issues/59 """ def foo(): pass foo._mark = True g = partial(foo) assert g._mark is True def test_wraps_dict(): """Checks that @wraps correctly propagates the __dict__""" def foo(): pass foo._mark = True @wraps(foo) def g(): pass assert g._mark is True def test_issue_62(): """https://github.com/smarie/python-makefun/issues/62""" def f(a, b): return a+b fp = partial(f, 0) assert fp(-1) == -1 def test_issue_63(): """https://github.com/smarie/python-makefun/issues/63""" def a(foo=float("inf")): pass @with_signature(signature(a)) def test(*args, **kwargs): return a(*args, **kwargs) def test_issue_66(): """Chain of @wraps with sig mod https://github.com/smarie/python-makefun/issues/66""" def a(foo): return foo + 1 assert a(1) == 2 # create a first wrapper that is signature-preserving @wraps(a) def wrapper(foo): return a(foo) - 1 assert wrapper(1) == 1 # the __wrapped__ attr is here: assert wrapper.__wrapped__ is a # create a second wrapper that is not signature-preserving @wraps(wrapper, append_args="bar") def second_wrapper(foo, bar): return wrapper(foo) + bar assert second_wrapper.__wrapped__ is wrapper assert "bar" in signature(second_wrapper).parameters assert second_wrapper(1, -1) == 0 def test_issue_pr_67(): """Test handcrafted for https://github.com/smarie/python-makefun/pull/67""" class CustomException(Exception): pass class Foo(object): def __init__(self, a=None): if a is None: raise CustomException() def __repr__(self): # this is a valid string but calling eval on it will raise an return "Foo()" f = Foo(a=1) # (1) The object can be represented but for some reason its repr can not be evaluated with pytest.raises(CustomException): eval(repr(f)) # (2) Lets check that this problem does not impact `makefun` def foo(a=Foo(a=1)): pass @wraps(foo, prepend_args="r") def bar(*args, **kwargs): pass bar(1) def test_issue_76(): def f(a): return a + 1 f2 = create_function("zoo(a)", f, func=f) assert f2(3) == 4 @pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python 3.6 or higher (async generator)") def test_issue_77_async_generator_wraps(): import asyncio from ._test_py36 import make_async_generator, make_async_generator_wrapper f = make_async_generator() wrapper = wraps(f)(make_async_generator_wrapper(f)) assert inspect.isasyncgenfunction(f) assert inspect.isasyncgenfunction(wrapper) assert asyncio.get_event_loop().run_until_complete(asyncio.ensure_future(wrapper(1).__anext__())) == 1 @pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python 3.6 or higher (async generator)") def test_issue_77_async_generator_partial(): import asyncio from ._test_py36 import make_async_generator f = make_async_generator() f_partial = partial(f, v=1) assert inspect.isasyncgenfunction(f) assert inspect.isasyncgenfunction(f_partial) assert asyncio.get_event_loop().run_until_complete(asyncio.ensure_future(f_partial().__anext__())) == 1 @pytest.mark.skipif(sys.version_info < (3, 7, 6), reason="The __wrapped__ behavior in get_type_hints being tested was not added until python 3.7.6.") def test_issue_85_wrapped_forwardref_annotation(): import typing from . import _issue_85_module @wraps(_issue_85_module.forwardref_method, remove_args=["bar"]) def wrapper(**kwargs): kwargs["bar"] = "x" # python 2 syntax to prevent syntax error. return _issue_85_module.forwardref_method(**kwargs) # Make sure the wrapper function works as expected assert wrapper(_issue_85_module.ForwardRef()).x == "defaultx" # Check that the type hints of the wrapper are ok with the forward reference correctly resolved expected_annotations = { "foo": _issue_85_module.ForwardRef, "return": _issue_85_module.ForwardRef, } assert typing.get_type_hints(wrapper) == expected_annotations def test_issue_91(): """This test should work also in python 2 ! """ assert is_identifier("_results_bag") assert is_identifier("hello__bag") def test_issue_98(): class A(str): def __str__(self): return 'custom str' def __repr__(self): return 'custom repr' def foo(a=A()): pass @wraps(foo) def test(*args, **kwargs): return foo(*args, **kwargs) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/tests/test_partial_and_macros.py����������������������������������������������0000664�0000000�0000000�00000012466�14644215064�0023163�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import functools import pytest import re import sys import makefun try: from inspect import signature except ImportError: from funcsigs import signature PY2 = sys.version_info < (3, ) # Python 3.13 dedents docstrings, earlier versions just strip initial # whitespace. Use a regexp to get a consistently dedented docstring # for comparison across Python versions. DOCSTRING_NORMALIZE_RE = re.compile(r"^ +", re.MULTILINE) def test_doc(): def foo(x, y): """ a `foo` function :param x: :param y: :return: """ return x + y ref_bar = functools.partial(foo, x=12) ref_sig_str = "(x=12, y)" if PY2 else "(*, x=12, y)" assert str(signature(ref_bar)) == ref_sig_str bar = makefun.partial(foo, x=12) # same behaviour - except in python 2 where our "KW_ONLY_ARG!" appear assert str(signature(bar)).replace("=KW_ONLY_ARG!", "") == str(signature(ref_bar)) bar.__name__ = 'bar' help(bar) with pytest.raises(TypeError): bar(1) assert bar(y=1) == 13 sig_actual_call = ref_sig_str.replace("*, ", "") assert DOCSTRING_NORMALIZE_RE.sub("", bar.__doc__) \ == """<This function is equivalent to 'foo%s', see original 'foo' doc below.> a `foo` function :param x: :param y: :return: """ % sig_actual_call def test_partial(): """Tests that `with_partial` works""" @makefun.with_partial(y='hello') def foo(x, y, a): """ a `foo` function :param x: :param y: :param a: :return: """ print(a) print(x, y) if not PY2: # true keyword-only with pytest.raises(TypeError): foo(1, 2) foo(1, a=2) help(foo) sig_actual_call = "(x, y='hello', a)" # if PY2 else "(x, *, y='hello', a)" assert DOCSTRING_NORMALIZE_RE.sub("", foo.__doc__.replace("=KW_ONLY_ARG!", "")) \ == """<This function is equivalent to 'foo%s', see original 'foo' doc below.> a `foo` function :param x: :param y: :param a: :return: """ % sig_actual_call def test_issue_57(): def f(b=0): """hey""" return b f.i = 1 # creating the decorator dec = makefun.wraps(functools.partial(f, b=2), func_name='foo') # applying the decorator n = dec(functools.partial(f, b=1)) # check metadata assert n.i == 1 # check signature sig_actual_call = "(b=2)" # sig = sig_actual_call if PY2 else "(*, b=2)" assert n.__doc__ == """<This function is equivalent to 'f%s', see original 'f' doc below.> hey""" % sig_actual_call # check implementation: the default value from the signature (from @wraps) is the one that applies here assert n() == 2 def test_create_with_partial(): def f(b=0): """hey""" return b f.i = 1 m = makefun.create_function("(b=-1)", functools.partial(f, b=2), **f.__dict__) assert str(signature(m)) == "(b=-1)" assert m() == -1 assert m.i == 1 # the doc remains untouched in create_function as opposed to wraps, this is normal assert m.__doc__ == functools.partial.__doc__ def test_args_order_and_kind(): """Make sure that the order remains ok""" def f(a, b, c, **d): return a + b + c + sum(d) # reference: functools.partial fp_ref = functools.partial(f, b=0) # except in python 2, all kwargs following the predefined arg become kw-only if sys.version_info < (3,): assert str(signature(fp_ref)) == "(a, b=0, c, **d)" else: assert str(signature(fp_ref)) == "(a, *, b=0, c, **d)" # our makefun.partial fp = makefun.partial(f, b=0) # same behaviour - except in python 2 where our "KW_ONLY_ARG!" appear assert str(signature(fp_ref)) == str(signature(fp)).replace("=KW_ONLY_ARG!", "") # positional-only behaviour if sys.version_info >= (3, 8): from ._test_py38 import make_pos_only_f f = make_pos_only_f() # it is possible to keyword-partialize a positional-only argument... fp_ref = functools.partial(f, b=0) # but 'signature' does not support it before Python 3.12.4 ! if sys.version_info < (3, 12, 4): with pytest.raises(ValueError): signature(fp_ref) else: assert str(signature(fp_ref)) == "(a, c, /, *, d, **e)" # TODO https://github.com/smarie/python-makefun/issues/107 # so we do not support it with pytest.raises(NotImplementedError): makefun.partial(f, b=0) # assert str(signature(fp_ref)) == str(signature(fp)) @pytest.mark.parametrize("is_generator", [False, True]) def test_simple_partial_copy(is_generator): """Test that when not providing any argument to partial, it is equivalent to wraps with new sig = None This test was extended to cover issue 79. """ if is_generator: def f1(a): yield a + 1 else: def f1(a): return a + 1 f2 = makefun.partial(f1) # make sure that this is the same as wraps # and same for the func attribute assert f2.func == f2.__wrapped__ == f1 f3 = makefun.wraps(f1)(f1) assert f3.__wrapped__ == f1 if is_generator: assert next(f2(1)) == next(f3(1)) == 2 else: assert f2(1) == f3(1) == 2 # the func attribute is there too f4 = functools.partial(f1) assert f2.func == f4.func ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-makefun-1.15.4/tests/test_so.py��������������������������������������������������������������0000664�0000000�0000000�00000023166�14644215064�0017761�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import print_function import sys from inspect import getmodule import pytest from makefun import create_function, wraps, partial, with_partial try: # python 3.3+ from inspect import signature, Signature, Parameter except ImportError: from funcsigs import signature, Signature, Parameter PY2 = sys.version_info < (3, ) def test_create_facades(capsys): """ Simple test to create multiple functions with the same body This corresponds to the answer at https://stackoverflow.com/questions/13184281/python-dynamic-function-creation-with-custom-names/55105893#55105893 :return: """ # generic core implementation def generic_impl(f, *args, **kwargs): print("This is generic impl called by %s" % f.__name__) # here you could use f.__name__ in a if statement to determine what to do if f.__name__ == "func1": print("called from func1 !") return args, kwargs my_module = getmodule(generic_impl) # generate 3 facade functions with various signatures for f_name, f_params in [("func1", "b, *, a"), ("func2", "*args, **kwargs"), ("func3", "c, *, a, d=None")]: if f_name in {"func1", "func3"} and sys.version_info < (3, 0): # Python 2 does not support function annotations; Python 3.0-3.4 do not support variable annotations. pass else: # the signature to generate f_sig = "%s(%s)" % (f_name, f_params) # create the function dynamically f = create_function(f_sig, generic_impl, inject_as_first_arg=True) # assign the symbol somewhere (local context, module...) setattr(my_module, f_name, f) # grab each function and use it if sys.version_info >= (3, 0): func1 = getattr(my_module, 'func1') assert func1(25, a=12) == ((), dict(b=25, a=12)) func2 = getattr(my_module, 'func2') assert func2(25, a=12) == ((25,), dict(a=12)) if sys.version_info >= (3, 0): func3 = getattr(my_module, 'func3') assert func3(25, a=12) == ((), dict(c=25, a=12, d=None)) captured = capsys.readouterr() with capsys.disabled(): print(captured.out) if sys.version_info >= (3, 0): assert captured.out == """This is generic impl called by func1 called from func1 ! This is generic impl called by func2 This is generic impl called by func3 """ else: assert captured.out == """This is generic impl called by func2 """ def test_so_decorator(): """ Tests that solution at https://stackoverflow.com/questions/739654/how-to-make-a-chain-of-function-decorators/1594484#1594484 actually works """ # from functools import wraps def makebold(fn): @wraps(fn) def wrapped(): return "<b>" + fn() + "</b>" return wrapped def makeitalic(fn): @wraps(fn) def wrapped(): return "<i>" + fn() + "</i>" return wrapped @makebold @makeitalic def hello(): """what?""" return "hello world" assert hello() == "<b><i>hello world</i></b>" assert hello.__name__ == "hello" help(hello) # the help and signature are preserved assert hasattr(hello, '__wrapped__') def test_so_facade(): def create_initiation_function(cls, gen_init): # (1) check which signature we want to create params = [Parameter('self', kind=Parameter.POSITIONAL_OR_KEYWORD)] for mandatory_arg_name in cls.__init_args__: params.append(Parameter(mandatory_arg_name, kind=Parameter.POSITIONAL_OR_KEYWORD)) for default_arg_name, default_arg_val in cls.__opt_init_args__.items(): params.append(Parameter(default_arg_name, kind=Parameter.POSITIONAL_OR_KEYWORD, default=default_arg_val)) sig = Signature(params) # (2) create the init function dynamically return create_function(sig, generic_init) # ----- let's use it def generic_init(self, *args, **kwargs): """Function to initiate a generic object""" assert len(args) == 0 for name, val in kwargs.items(): setattr(self, name, val) class my_class: __init_args__ = ["x", "y"] __opt_init_args__ = {"my_opt": None} my_class.__init__ = create_initiation_function(my_class, generic_init) # check o1 = my_class(1, 2) assert vars(o1) == {'y': 2, 'x': 1, 'my_opt': None} o2 = my_class(1, 2, 3) assert vars(o2) == {'y': 2, 'x': 1, 'my_opt': 3} o3 = my_class(my_opt='hello', y=3, x=2) assert vars(o3) == {'y': 3, 'x': 2, 'my_opt': 'hello'} def test_so_sig_preserving(capsys): """ Tests that the answer at https://stackoverflow.com/a/55163391/7262247 is correct """ def my_decorator(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) wrapper._decorator_name_ = 'my_decorator' return wrapper @my_decorator def my_func(x): """my function""" print('hello %s' % x) assert my_func._decorator_name_ == 'my_decorator' help(my_func) captured = capsys.readouterr() with capsys.disabled(): print(captured.out) assert captured.out == """Help on function my_func in module tests.test_so: my_func(x) my function """ def test_sig_preserving_2(capsys): """ Checks that answer at https://stackoverflow.com/a/55163816/7262247 works """ def args_as_ints(func): @wraps(func) def wrapper(*args, **kwargs): print("wrapper executes") # convert all to int. note that in a signature-preserving wrapper almost all args will come as kwargs args = [int(x) for x in args] kwargs = dict((k, int(v)) for k, v in kwargs.items()) return func(*args, **kwargs) return wrapper @args_as_ints def funny_function(x, y, z=3): """Computes x*y + 2*z""" return x * y + 2 * z print(funny_function("3", 4.0, z="5")) # 22 help(funny_function) # Help on function funny_function in module __main__: # # funny_function(x, y, z=3) # Computes x*y + 2*z with pytest.raises(TypeError): funny_function(0) # TypeError: funny_function() takes at least 2 arguments (1 given) captured = capsys.readouterr() with capsys.disabled(): print(captured.out) assert captured.out == """wrapper executes 22 Help on function funny_function in module tests.test_so: funny_function(x, y, z=3) Computes x*y + 2*z """ def test_so_partial(capsys): """ Tests that the answer at https://stackoverflow.com/a/55165541/7262247 is correct """ def foo(a, b, c=1): """Return (a+b)*c.""" return (a + b) * c bar10_p = partial(foo, b=10) assert bar10_p(0) == 10 assert bar10_p(0, c=2) == 20 help(bar10_p) captured = capsys.readouterr() with capsys.disabled(): print(captured.out) sig_actual_call = "(a, b=10, c=1)" sig = sig_actual_call if PY2 else "(a, *, b=10, c=1)" assert captured.out == """Help on function foo in module tests.test_so: foo{sig} <This function is equivalent to 'foo{sig_actual_call}', see original 'foo' doc below.> Return (a+b)*c. """.format(sig=sig, sig_actual_call=sig_actual_call) def test_so_partial2(capsys): """ Tests that the solution at https://stackoverflow.com/a/55161579/7262247 works (the one using makefun only. for the other two, see test.so.py in decopatch project) """ @with_partial(a='hello', b='world') def test(a, b, x, y): print(a, b) print(x, y) if not PY2: # true keywords-only with pytest.raises(TypeError): test(1, 2) # TypeError: test() takes 0 positional arguments but 2 were given test(x=1, y=2) help(test) @with_partial(a='hello', b='world') def test(a, b, x, y): """Here is a doc""" print(a, b) print(x, y) help(test) captured = capsys.readouterr() with capsys.disabled(): print(captured.out) sig_actual_call = "(a='hello', b='world', x, y)" sig = "(a='hello', b='world', x=KW_ONLY_ARG!, y=KW_ONLY_ARG!)" if PY2 else "(*, a='hello', b='world', x, y)" ref_str = """hello world 1 2 Help on function test in module tests.test_so: test{sig} <This function is equivalent to 'test{sig_actual_call}'.> Help on function test in module tests.test_so: test{sig} <This function is equivalent to 'test{sig_actual_call}', see original 'test' doc below.> Here is a doc """.format(sig=sig, sig_actual_call=sig_actual_call) if (3, 0) <= sys.version_info < (3, 6): # in older versions of python, the order of **kwargs is not guaranteed (see PEP 468) assert captured.out.replace('a=hello', 'b=world') == ref_str.replace('a=hello', 'b=world') else: assert captured.out == ref_str def test_so_partial_posargs(capsys): """Checks that the generated documentation is ok even in case of positional preset arg""" def test(a, b, x, y): print(a, b) print(x, y) fp = partial(test, 'hello', b='world') help(fp) captured = capsys.readouterr() with capsys.disabled(): print(captured.out) sig_actual_call = "('hello', b='world', x, y)" sig = "(b='world', x=KW_ONLY_ARG!, y=KW_ONLY_ARG!)" if PY2 else "(*, b='world', x, y)" ref_str = """Help on function test in module tests.test_so: test{sig} <This function is equivalent to 'test{sig_actual_call}'.> """.format(sig=sig, sig_actual_call=sig_actual_call) assert captured.out == ref_str ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������