pax_global_header00006660000000000000000000000064142073571430014520gustar00rootroot0000000000000052 comment=1d4352f7c9b3e6a46cfcfefed005ab541ba501c0 python-decopatch-1.4.10/000077500000000000000000000000001420735714300150545ustar00rootroot00000000000000python-decopatch-1.4.10/.github/000077500000000000000000000000001420735714300164145ustar00rootroot00000000000000python-decopatch-1.4.10/.github/workflows/000077500000000000000000000000001420735714300204515ustar00rootroot00000000000000python-decopatch-1.4.10/.github/workflows/base.yml000066400000000000000000000133471420735714300221160ustar00rootroot00000000000000# .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 jobs: # pre-job to read nox tests matrix - see https://stackoverflow.com/q/66747359/7262247 list_nox_test_sessions: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v1 with: python-version: 3.7 architecture: x64 - name: Install noxfile requirements shell: bash -l {0} run: pip install -r noxfile-requirements.txt - name: List 'tests' nox sessions id: set-matrix run: echo "::set-output name=matrix::$(nox -s gha_list -- tests)" 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: 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 }} # ${{ matrix.name_suffix }} runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 # Conda install - name: Install conda v3.7 uses: conda-incubator/setup-miniconda@v2 with: # auto-update-conda: true python-version: 3.7 activate-environment: noxenv - run: conda info shell: bash -l {0} # so that conda works - run: conda list shell: bash -l {0} # so that conda works # Nox install + run - name: Install noxfile requirements shell: bash -l {0} # so that conda works run: pip install -r noxfile-requirements.txt - run: conda list shell: bash -l {0} # so that conda works - run: nox -s "${{ matrix.nox_session }}" shell: bash -l {0} # so that conda works # 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 wont be shared uses: actions/upload-artifact@master with: name: reports_dir path: ./docs/reports 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" - uses: actions/checkout@v2 with: fetch-depth: 0 # so that gh-deploy works # 1) retrieve the reports generated previously - name: Retrieve reports uses: actions/download-artifact@master with: name: reports_dir path: ./docs/reports # Conda install - name: Install conda v3.7 uses: conda-incubator/setup-miniconda@v2 with: # auto-update-conda: true python-version: 3.7 activate-environment: noxenv - run: conda info shell: bash -l {0} # so that conda works - run: conda list shell: bash -l {0} # so that conda works # Nox install - name: Install noxfile requirements shell: bash -l {0} # so that conda works run: pip install -r noxfile-requirements.txt - run: conda list shell: bash -l {0} # so that conda works # 5) Run the flake8 report and badge - name: Run flake8 analysis and generate corresponding badge shell: bash -l {0} # so that conda works 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') shell: bash -l {0} # so that conda works 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-decopatch/::" # Correct paths EOF - name: \[not on TAG\] Publish coverage report if: github.event_name == 'push' && startsWith(github.ref, 'refs/heads') uses: codecov/codecov-action@v1 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') shell: bash -l {0} # so that conda works 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@release/v1 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-decopatch-1.4.10/.gitignore000066400000000000000000000036321420735714300170500ustar00rootroot00000000000000# 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/decopatch/_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-decopatch-1.4.10/.travis.yml000066400000000000000000000115661420735714300171760ustar00rootroot00000000000000language: python cache: pip matrix: fast_finish: true include: # - python: 2.6 - python: 2.7 # - python: 3.3 # - python: 3.4 - python: 3.5 - python: 3.6 - python: 3.7 dist: xenial sudo: true env: global: - GH_REF: git@github.com:smarie/python-decopatch.git before_install: # (a) linux dependencies - sudo apt-get install ant - sudo apt-get install ant-optional install: - pip list # needs to be installed beforehand - pip install setuptools_scm - python ci_tools/py_install.py pip ci_tools/requirements-pip.txt # this does not work anymore on python 2 so lets only do it when needed - if [ "${TRAVIS_PYTHON_VERSION}" = "3.5" ]; then pip install mkdocs-material mkdocs; fi; # travis-specific installs - pip install PyGithub # for ci_tools/github_release.py - pip install codecov # See https://github.com/codecov/example-python. - pip list script: # - coverage run tests.py - pip install . - python -c "import os; os.chdir('..'); import decopatch" # ***tests*** # - coverage run tests.py # - pytest --junitxml=reports/junit/junit.xml --html=reports/junit/report.html --cov-report term-missing --cov=./decopatch -v decopatch/tests/ # now done in a dedicated script to capture exit code 1 and transform it to 0 - chmod a+x ./ci_tools/run_tests.sh - sh ./ci_tools/run_tests.sh - python ci_tools/generate-junit-badge.py 100 # generates the badge for the test results and fail build if less than x% after_success: # ***reporting*** # - junit2html junit.xml testrun.html output is really not nice - ant -f ci_tools/generate-junit-html.xml # generates the html for the test results. Actually we dont use it anymore - codecov #- pylint decopatch # note that at the moment the report is simply lost, we dont transform the result into anything # ***documentation*** - mkdocs build -f docs/mkdocs.yml - mv reports/junit docs/ # not in site/ anymore so that we do not need to use --dirty (it breaks client-side search) # mkdocs gh-deploy requires special care : # ---grant the possibility to push on the repo--- - openssl aes-256-cbc -K $encrypted_d49bee21f6e2_key -iv $encrypted_d49bee21f6e2_iv -in ci_tools/github_travis_rsa.enc -out ci_tools/github_travis_rsa -d # If the output file does not exist, that is because the secret is invalid. This can happen in forked repos so do not fail the build - | if [ -s "ci_tools/github_travis_rsa" ]; then chmod 600 ci_tools/github_travis_rsa eval `ssh-agent -s` # launch the authentication agent ssh-add ci_tools/github_travis_rsa # register the decrypted key git config user.name "Automatic Publish" git config user.email "sylvain.marie@schneider-electric.com" git remote add gh-remote "${GH_REF}"; git fetch gh-remote && git fetch gh-remote gh-pages:gh-pages; # make sure we have the latest gh-remote # push but only if this is not a build triggered by a pull request # note: do not use the --dirty flag as it breaks client-side search if [ "${TRAVIS_PULL_REQUEST}" = "false" ] && [ "${TRAVIS_PYTHON_VERSION}" = "3.5" ]; then echo "Pushing to github"; PYTHONPATH=decopatch/ mkdocs gh-deploy -v -f docs/mkdocs.yml --remote-name gh-remote; git push gh-remote gh-pages; fi; else echo "File 'ci_tools/github_travis_rsa' has not been created, please check your encrypted repo token in .travis.yml, on the line starting with 'openssl aes-256-cbc...'" fi # -- create the _version.py file # - python ci_tools/write_version.py ./decopatch deploy: # Deploy on PyPI on tags - provider: pypi user: "smarie" password: secure: "DNcKQoEPlxYyELQRemhvBWEZ/ve57qiLvd/Ao6xd78LG1dEKOSBpm23HnNCC2vDERBGQBBOnIQoTqwera34Eq0rqAfmy2n/H/8PhJSVD3HygoMyRU1eqa/up2W9DvEEUfRnomwTEmJjgmMvRZ/rd1mZaeGPftgKAittUyYyPLB3R0B1aK8grcfQgbG3FURktM6jy8PJPuSFYjwocdrdKNFJVvnsUOYPMLHE30tYGbn9gl4WuwrvjTc9svSsu7YxlZvgCdyr+73dyAKbwM4Qan42Tv3aQ4LgISQIGTAT7JiE6L0K5Ny0iP8CAIswzHIl5YD8+7OUA7wh0K+FT9sLw/itl96lgla+wSBiZSFm9CSXFC5XiyJOrmUb6zZfWBzyOMGQOhem77AzpMva3fDsHYRDnNLn6cZKZxTZgTRKZvcoUFwO99tFdnQapSK52aSVNWmctdwL08a4Ws6sxF85iH3V3DNTQmSpz3Ej013OXwZEvhhAZZAEU8Vg9GH4OlcP1hW52jnYMZeOOj1z1M0aU8A2UaNvvRjztKIQ/a28e43mlVGDrGHs9xhyIAPWxFfdyFouNvbVJkRr4gTTC9PtYlwuhtAcpkN+IJUf0tMV2extYl8SKF/PiIcqnL4HuWM+Vh5JIi2rVqY5U3tMubvQAQtDmEP/oxPoD38Sb0BK7lAo=" on: tags: true python: 3.5 #only one of the builds have to be deployed # server: https://test.pypi.org/legacy/ distributions: "sdist bdist_wheel" # Create a github release on tags - provider: script script: python ci_tools/github_release.py -s $GITHUB_TOKEN --repo-slug smarie/python-decopatch -cf ./docs/changelog.md -d https://smarie.github.io/python-decopatch/changelog/ $TRAVIS_TAG skip_cleanup: true on: tags: true python: 3.5 #only one of the builds have to be deployed notifications: email: on_success: never # options: [always|never|change] default: always python-decopatch-1.4.10/LICENSE000066400000000000000000000030161420735714300160610ustar00rootroot00000000000000BSD 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-decopatch-1.4.10/README.md000066400000000000000000000142211420735714300163330ustar00rootroot00000000000000# python-decopatch *python decorators made easy.* [![Python versions](https://img.shields.io/pypi/pyversions/decopatch.svg)](https://pypi.python.org/pypi/decopatch/) [![Build Status](https://github.com/smarie/python-decopatch/actions/workflows/base.yml/badge.svg)](https://github.com/smarie/python-decopatch/actions/workflows/base.yml) [![Tests Status](https://smarie.github.io/python-decopatch/reports/junit/junit-badge.svg?dummy=8484744)](https://smarie.github.io/python-decopatch/reports/junit/report.html) [![Coverage Status](https://smarie.github.io/python-decopatch/reports/coverage/coverage-badge.svg?dummy=8484744)](https://smarie.github.io/python-decopatch/reports/coverage/index.html) [![codecov](https://codecov.io/gh/smarie/python-decopatch/branch/main/graph/badge.svg)](https://codecov.io/gh/smarie/python-decopatch) [![Flake8 Status](https://smarie.github.io/python-decopatch/reports/flake8/flake8-badge.svg?dummy=8484744)](https://smarie.github.io/python-decopatch/reports/flake8/index.html) [![Documentation](https://img.shields.io/badge/doc-latest-blue.svg)](https://smarie.github.io/python-decopatch/) [![PyPI](https://img.shields.io/pypi/v/decopatch.svg)](https://pypi.python.org/pypi/decopatch/) [![Downloads](https://pepy.tech/badge/decopatch)](https://pepy.tech/project/decopatch) [![Downloads per week](https://pepy.tech/badge/decopatch/week)](https://pepy.tech/project/decopatch) [![GitHub stars](https://img.shields.io/github/stars/smarie/python-decopatch.svg)](https://github.com/smarie/python-decopatch/stargazers) **This is the readme for developers.** The documentation for users is available here: [https://smarie.github.io/python-decopatch/](https://smarie.github.io/python-decopatch/) ## 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-decopatch/issues](https://github.com/smarie/python-decopatch/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-decopatch/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-decopatch/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-decopatch.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-decopatch.git --no-commit ``` Finally review the changes, possibly perform some modifications, and commit. python-decopatch-1.4.10/ci_tools/000077500000000000000000000000001420735714300166675ustar00rootroot00000000000000python-decopatch-1.4.10/ci_tools/.pylintrc000066400000000000000000000420051420735714300205350ustar00rootroot00000000000000[MASTER] # Specify a configuration file. #rcfile= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). # init-hook="import decopatch" # 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-decopatch-1.4.10/ci_tools/check_python_version.py000066400000000000000000000023721420735714300234700ustar00rootroot00000000000000import 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-decopatch-1.4.10/ci_tools/flake8-requirements.txt000066400000000000000000000007501420735714300233250ustar00rootroot00000000000000setuptools_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-decopatch-1.4.10/ci_tools/github_release.py000066400000000000000000000131721420735714300222270ustar00rootroot00000000000000# 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-decopatch/releases/13928525' # release.html_url # 'https://github.com/smarie/python-decopatch/releases/tag/0.5.0' # release.tarball_url # 'https://api.github.com/repos/smarie/python-decopatch/tarball/0.5.0' # release.zipball_url # 'https://api.github.com/repos/smarie/python-decopatch/zipball/0.5.0' # release.upload_url # 'https://uploads.github.com/repos/smarie/python-decopatch/releases/13928525/assets{?name,label}' if __name__ == '__main__': create_or_update_release() ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/ci_tools/nox_utils.py�������������������������������������������������������0000664�0000000�0000000�00000071063�14207357143�0021274�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from itertools import product import asyncio from collections import namedtuple from inspect import signature, isfunction import logging from pathlib import Path import shutil import subprocess import sys import os from typing import Sequence, Dict, Union, Iterable, Mapping, Any, IO, Tuple, Optional, List from makefun import wraps, remove_signature_parameters, add_signature_parameters import nox from nox.sessions import Session nox_logger = logging.getLogger("nox") PY27, PY35, PY36, PY37, PY38, PY39, PY310 = "2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10" DONT_INSTALL = "dont_install" def power_session( func=None, envs=None, grid_param_name="env", python=None, py=None, reuse_venv=None, name=None, venv_backend=None, venv_params=None, logsdir=None, **kwargs ): """A nox.session on steroids :param func: :param envs: a dictionary {key: dict_of_params} where key is either the python version of a tuple (python version, grid id) and all keys in the dict_of_params must be the same in all entries. The decorated function should have one parameter for each of these keys, they will be injected with the value. :param grid_param_name: when the key in `envs` is a tuple, this name will be the name of the generated parameter to iterate through the various combinations for each python version. :param python: :param py: :param reuse_venv: :param name: :param venv_backend: :param venv_params: :param logsdir: :param kwargs: :return: """ if func is not None: return power_session()(func) else: def combined_decorator(f): # replace Session with PowerSession f = with_power_session(f) # open a log file for the session, use it to stream the commands stdout and stderrs, # and possibly inject the log file in the session function if logsdir is not None: f = with_logfile(logs_dir=logsdir)(f) # decorate with @nox.session and possibly @nox.parametrize to create the grid return nox_session_with_grid(python=python, py=py, envs=envs, reuse_venv=reuse_venv, name=name, grid_param_name=grid_param_name, venv_backend=venv_backend, venv_params=venv_params, **kwargs)(f) return combined_decorator def with_power_session(f=None): """ A decorator to patch the session objects in order to add all methods from Session2""" if f is not None: return with_power_session()(f) def _decorator(f): @wraps(f) def _f_wrapper(**kwargs): # patch the session arg PowerSession.patch(kwargs['session']) # finally execute the session return f(**kwargs) return _f_wrapper return _decorator class PowerSession(Session): """ Our nox session improvements """ # ------------ commandline runners ----------- def run2(self, command: Union[Iterable[str], str], logfile: Union[bool, str, Path] = True, **kwargs): """ An improvement of session.run that is able to - automatically split the provided command if it is a string - use a log file :param command: :param logfile: None/False (normal nox behaviour), or True (using nox file handler), or a file path. :param kwargs: :return: """ if isinstance(command, str): command = command.split(' ') self.run(*command, logfile=logfile, **kwargs) def run_multi(self, cmds: str, logfile: Union[bool, str, Path] = True, **kwargs): """ An improvement of session.run that is able to - support multiline strings - use a log file :param cmds: :param logfile: None/False (normal nox behaviour), or True (using nox file handler), or a file path. :param kwargs: :return: """ for cmdline in (line for line in cmds.splitlines() if line): self.run2(cmdline, logfile=logfile, **kwargs) # ------------ requirements installers ----------- def install_reqs( self, # 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: self.install_any("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: self.install_any("setup.cfg#setup_requires", setup_cfg.setup_requires, use_conda_for=toml_use_conda_for, versions_dct=versions_dct) if install: self.install_any("setup.cfg#install_requires", setup_cfg.install_requires, use_conda_for=toml_use_conda_for, versions_dct=versions_dct) if tests: self.install_any("setup.cfg#tests_requires", setup_cfg.tests_requires, use_conda_for=toml_use_conda_for, versions_dct=versions_dct) for extra in extras: self.install_any("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: self.install_any(phase, phase_reqs, use_conda_for=toml_use_conda_for, versions_dct=versions_dct) def install_any(self, phase_name: str, pkgs: Sequence[str], use_conda_for: Sequence[str] = (), versions_dct: Dict[str, str] = None, logfile: Union[bool, str, Path] = True, ): """Install the `pkgs` provided with `session.install(*pkgs)`, except for those present in `use_conda_for`""" nox_logger.debug("\nAbout to install *%s* requirements: %s.\n " "Conda pkgs are %s" % (phase_name, pkgs, 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] # install on conda... if the session uses conda backend if not isinstance(self.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)) self.conda_install2(*conda_pkgs, logfile=logfile) 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)) self.install2(*pip_pkgs, logfile=logfile) def conda_install2(self, *conda_pkgs, logfile: Union[bool, str, Path] = True, **kwargs ): """ Same as session.conda_install() but with support for `logfile`. :param conda_pkgs: :param logfile: None/False (normal nox behaviour), or True (using nox file handler), or a file path. :return: """ return self.conda_install(*conda_pkgs, logfile=logfile, **kwargs) def install2(self, *pip_pkgs, logfile: Union[bool, str, Path] = True, **kwargs ): """ Same as session.install() but with support for `logfile`. :param pip_pkgs: :param logfile: None/False (normal nox behaviour), or True (using nox file handler), or a file path. :return: """ return self.install(*pip_pkgs, logfile=logfile, **kwargs) def get_session_id(self): """Return the session id""" return Path(self.bin).name @classmethod def is_power_session(cls, session: Session): return PowerSession.install2.__name__ in session.__dict__ @classmethod def patch(cls, session: Session): """ Add all methods from this class to the provided object. Note that we could instead have created a proper proxy... but complex for not a lot of benefit. :param session: :return: """ if not cls.is_power_session(session): for m_name, m in cls.__dict__.items(): if not isfunction(m): continue if m is cls.patch: continue if not hasattr(session, m_name): setattr(session.__class__, m_name, m) return True # ------------- requirements related def read_pyproject_toml(): """ 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'] conda_pkgs = pyproject['tool']['conda']['conda_packages'] 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] # ------------- log related def with_logfile(logs_dir: Path, logfile_arg: str = "logfile", logfile_handler_arg: str = "logfilehandler" ): """ A decorator to inject a logfile""" def _decorator(f): # check the signature of f foo_sig = signature(f) needs_logfile_injection = logfile_arg in foo_sig.parameters needs_logfilehandler_injection = logfile_handler_arg in foo_sig.parameters # modify the exposed signature if needed new_sig = None if needs_logfile_injection: new_sig = remove_signature_parameters(foo_sig, logfile_arg) if needs_logfilehandler_injection: new_sig = remove_signature_parameters(foo_sig, logfile_handler_arg) @wraps(f, new_sig=new_sig) def _f_wrapper(**kwargs): # find the session arg session = kwargs['session'] # type: Session # add file handler to logger logfile = logs_dir / ("%s.log" % PowerSession.get_session_id(session)) error_logfile = logfile.with_name("ERROR_%s" % logfile.name) success_logfile = logfile.with_name("SUCCESS_%s" % logfile.name) # delete old files if present for _f in (logfile, error_logfile, success_logfile): if _f.exists(): _f.unlink() # add a FileHandler to the logger logfile_handler = log_to_file(logfile) # inject the log file / log file handler in the args: if needs_logfile_injection: kwargs[logfile_arg] = logfile if needs_logfilehandler_injection: kwargs[logfile_handler_arg] = logfile_handler # finally execute the session try: res = f(**kwargs) except Exception as e: # close and detach the file logger and rename as ERROR_....log remove_file_logger() logfile.rename(error_logfile) raise e else: # close and detach the file logger and rename as SUCCESS_....log remove_file_logger() logfile.rename(success_logfile) return res return _f_wrapper return _decorator def log_to_file(file_path: Union[str, Path] ): """ Closes and removes all file handlers from the nox logger, and add a new one to the provided file path :param file_path: :return: """ for h in list(nox_logger.handlers): if isinstance(h, logging.FileHandler): h.close() nox_logger.removeHandler(h) fh = logging.FileHandler(str(file_path), mode='w') nox_logger.addHandler(fh) return fh def get_current_logfile_handler(): """ Returns the current unique log file handler (see `log_to_file`) """ for h in list(nox_logger.handlers): if isinstance(h, logging.FileHandler): return h return None def get_log_file_stream(): """ Returns the output stream for the current log file handler if any (see `log_to_file`) """ h = get_current_logfile_handler() if h is not None: return h.stream return None def remove_file_logger(): """ Closes and detaches the current logfile handler :return: """ h = get_current_logfile_handler() if h is not None: h.close() nox_logger.removeHandler(h) # ------------ environment grid / parametrization related def nox_session_with_grid(python = None, py = None, envs: Mapping[str, Mapping[str, Any]] = None, reuse_venv: Optional[bool] = None, name: Optional[str] = None, venv_backend: Any = None, venv_params: Any = None, grid_param_name: str = None, **kwargs ): """ Since nox is not yet capable to define a build matrix with python and parameters mixed in the same parametrize this implements it with a dirty hack. To remove when https://github.com/theacodes/nox/pull/404 is complete :param envs: :param env_python_key: :return: """ if envs is None: # Fast track default to @nox.session return nox.session(python=python, py=py, reuse_venv=reuse_venv, name=name, venv_backend=venv_backend, venv_params=venv_params, **kwargs) else: # Current limitation : session param names can be 'python' or 'py' only if py is not None or python is not None: raise ValueError("`python` session argument can not be provided both directly and through the " "`env` with `session_param_names`") # First examine the env and collect the parameter values for python all_python = [] all_params = [] env_contents_names = None has_parameter = None for env_id, env_params in envs.items(): # consistency checks for the env_id if has_parameter is None: has_parameter = isinstance(env_id, tuple) else: if has_parameter != isinstance(env_id, tuple): raise ValueError("All keys in env should be tuples, or not be tuples. Error for %r" % env_id) # retrieve python version and parameter if not has_parameter: if env_id not in all_python: all_python.append(env_id) else: if len(env_id) != 2: raise ValueError("Only a size-2 tuple can be used as env id") py_id, param_id = env_id if py_id not in all_python: all_python.append(py_id) if param_id not in all_params: all_params.append(param_id) # consistency checks for the dict contents. if env_contents_names is None: env_contents_names = set(env_params.keys()) else: if env_contents_names != set(env_params.keys()): raise ValueError("Environment %r parameters %r does not match parameters in the first environment: %r" % (env_id, env_contents_names, set(env_params.keys()))) if has_parameter and not grid_param_name: raise ValueError("You must provide a grid parameter name when the env keys are tuples.") def _decorator(f): s_name = name if name is not None else f.__name__ for pyv, _param in product(all_python, all_params): if (pyv, _param) not in envs: # create a dummy folder to avoid creating a useless venv ? env_dir = Path(".nox") / ("%s-%s-%s-%s" % (s_name, pyv.replace('.', '-'), grid_param_name, _param)) env_dir.mkdir(parents=True, exist_ok=True) # check the signature of f foo_sig = signature(f) missing = env_contents_names - set(foo_sig.parameters) if len(missing) > 0: raise ValueError("Session function %r does not contain environment parameter(s) %r" % (f.__name__, missing)) # modify the exposed signature if needed new_sig = None if len(env_contents_names) > 0: new_sig = remove_signature_parameters(foo_sig, *env_contents_names) if has_parameter: if grid_param_name in foo_sig.parameters: raise ValueError("Internal error, this parameter has a reserved name: %r" % grid_param_name) else: new_sig = add_signature_parameters(new_sig, last=(grid_param_name,)) @wraps(f, new_sig=new_sig) def _f_wrapper(**kwargs): # find the session arg session = kwargs['session'] # type: Session # get the versions to use for this environment try: if has_parameter: grid_param = kwargs.pop(grid_param_name) params_dct = envs[(session.python, grid_param)] else: params_dct = envs[session.python] except KeyError: # Skip this session, it is a dummy one nox_logger.warning( "Skipping configuration, this is not supported in python version %r" % session.python) return # inject the parameters in the args: kwargs.update(params_dct) # finally execute the session return f(**kwargs) if has_parameter: _f_wrapper = nox.parametrize(grid_param_name, all_params)(_f_wrapper) _f_wrapper = nox.session(python=all_python, reuse_venv=reuse_venv, name=name, venv_backend=venv_backend, venv_params=venv_params)(_f_wrapper) return _f_wrapper return _decorator # ----------- 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 # --- the patch of popen able to tee to logfile -- import nox.popen as nox_popen_module orig_nox_popen = nox_popen_module.popen class LogFileStreamCtx: def __init__(self, logfile_stream): self.logfile_stream = logfile_stream def __enter__(self): return self.logfile_stream def __exit__(self, exc_type, exc_val, exc_tb): pass def patched_popen( args: Sequence[str], env: Mapping[str, str] = None, silent: bool = False, stdout: Union[int, IO] = None, stderr: Union[int, IO] = subprocess.STDOUT, logfile: Union[bool, str, Path] = None, **kwargs ) -> Tuple[int, str]: """ Our patch of nox.popen.popen(). Current behaviour in `nox` is - when `silent=True` (default), process err is redirected to STDOUT and process out is captured in a PIPE and sent to the logger (that does not displaying it :) ) - when `silent=False` (explicitly set, or when nox is run with verbose flag), process out and process err are both redirected to STDOUT. Our implementation allows us to be a little more flexible: - if logfile is True or a string/Path, both process err and process out are both TEE-ed to logfile - at the same time, the above behaviour remains. :param args: :param env: :param silent: :param stdout: :param stderr: :param logfile: None/False (normal nox behaviour), or True (using nox file handler), or a file path. :return: """ logfile_stream = get_log_file_stream() if logfile in (None, False) or (logfile is True and logfile_stream is None): # execute popen as usual return orig_nox_popen(args=args, env=env, silent=silent, stdout=stdout, stderr=stderr, **kwargs) else: # we'll need to tee the popen if logfile is True: ctx = LogFileStreamCtx else: ctx = lambda _: open(logfile, "a") with ctx(logfile_stream) as log_file_stream: if silent and stdout is not None: raise ValueError( "Can not specify silent and stdout; passing a custom stdout always silences the commands output in " "Nox's log." ) shell = kwargs.get("shell", False) if shell: raise ValueError("Using shell=True is not yet supported with async streaming to log files") if stdout is not None or stderr is not subprocess.STDOUT: raise ValueError("Using custom streams is not yet supported with async popen") # old way # proc = subprocess.Popen(args, env=env, stdout=stdout, stderr=stderr) # New way: use asyncio to stream correctly # Note: if keyboard interrupts do not work we should check # https://mail.python.org/pipermail/async-sig/2017-August/000374.html maybe or the following threads. # define the async coroutines async def async_popen(): process = await asyncio.create_subprocess_exec(*args, env=env, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, **kwargs) # bind the out and err streams - see https://stackoverflow.com/a/59041913/7262247 # to mimic nox behaviour we only use a single capturing list outlines = [] await asyncio.wait([ # process out is only redirected to STDOUT if not silent _read_stream(process.stdout, lambda l: tee(l, sinklist=outlines, sinkstream=log_file_stream, quiet=silent, verbosepipe=sys.stdout)), # process err is always redirected to STDOUT (quiet=False) with a specific label _read_stream(process.stderr, lambda l: tee(l, sinklist=outlines, sinkstream=log_file_stream, quiet=False, verbosepipe=sys.stdout, label="ERR:")) ]) return_code = await process.wait() # make sur the process has ended and retrieve its return code return return_code, outlines # run the coroutine in the event loop loop = asyncio.get_event_loop() return_code, outlines = loop.run_until_complete(async_popen()) # just in case, flush everything log_file_stream.flush() sys.stdout.flush() sys.stderr.flush() if silent: # same behaviour as in nox: this will be passed to the logger, and it will act depending on verbose flag out = "\n".join(outlines) if len(outlines) > 0 else "" else: # already written to stdout, no need to capture out = "" return return_code, out async def _read_stream(stream, callback): """Helper async coroutine to read from a stream line by line and write them in callback""" while True: line = await stream.readline() if line: callback(line) else: break def tee(linebytes, sinklist, sinkstream, verbosepipe, quiet, label=""): """ Helper routine to read a line, decode it, and append it to several sinks: - an optional `sinklist` list that will receive the decoded string in its "append" method - an optional `sinkstream` stream that will receive the decoded string in its "writelines" method - an optional `verbosepipe` stream that will receive only when quiet=False, the decoded string through a print append it to the sink, and if quiet=False, write it to pipe too. """ line = linebytes.decode('utf-8').rstrip() if sinklist is not None: sinklist.append(line) if sinkstream is not None: sinkstream.write(line + "\n") sinkstream.flush() if not quiet and verbosepipe is not None: print(label, line, file=verbosepipe) verbosepipe.flush() def patch_popen(): nox_popen_module.popen = patched_popen from nox.command import popen if popen is not patched_popen: nox.command.popen = patched_popen # change event loop on windows # see https://stackoverflow.com/a/44639711/7262247 # and https://docs.python.org/3/library/asyncio-platforms.html#subprocess-support-on-windows if 'win32' in sys.platform: # Windows specific event-loop policy & cmd asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) # cmds = [['C:/Windows/system32/HOSTNAME.EXE']] # loop = asyncio.ProactorEventLoop() # asyncio.set_event_loop(loop) patch_popen() �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/docs/�����������������������������������������������������������������������0000775�0000000�0000000�00000000000�14207357143�0016004�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/docs/changelog.md�����������������������������������������������������������0000664�0000000�0000000�00000015037�14207357143�0020263�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Changelog ### 1.4.10 - Type hints step 1 - `@function_decorator` now has proper type hints. This is a first step towards fixing [#22](https://github.com/smarie/python-decopatch/issues/22). PR [#23](https://github.com/smarie/python-decopatch/pull/23) by [last-partizan](https://github.com/last-partizan). ### 1.4.9 - New layout and CI + Compatibility fixes. - Enabling the "stack introspection" beta feature with `enable_stack_introspection=True` now raises an explicit `NotImplementedError` on python 3.8+. Fixed [#26](https://github.com/smarie/python-decopatch/issues/26). - Migrated to the new project layout with nox, github-actions and separation of `src` and `tests`. Fixed test suite with `pytest-cases>=3`. Fixed [#24](https://github.com/smarie/python-decopatch/issues/24) and [#20](https://github.com/smarie/python-decopatch/issues/20). ### 1.4.8 - better packaging - 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 [#19](https://github.com/smarie/python-decopatch/issues/19) ### 1.4.7 - pyproject.toml [raddessi](https://github.com/raddessi) added a `pyproject.toml` - thanks! Fixed [pytest-cases#65](https://github.com/smarie/python-pytest-cases/issues/65). ### 1.4.6 - Bug fix Fixed decorated object injection issue when var-positional arguments are located before it in the signature. Fixed [#14](https://github.com/smarie/python-decopatch/issues/14). Added `__version__` attribute to comply with PEP396, following [this guide](https://smarie.github.io/python-getversion/#package-versioning-best-practices). Fixes [#15](https://github.com/smarie/python-decopatch/issues/15). PyPI supports markdown via `long_description_content_type`: pypandoc is not required anymore. Thanks [minrk](https://github.com/smarie/python-makefun/pull/44)! ### 1.4.5 - Performance improvement * Huge performance improvement for the `enable_stack_introspection=True` mode. It also now supports classes correctly. ### 1.4.4 - Reverted varpositional bugfixes now that they are handled in makefun ### 1.4.3 - Bugfix in nested mode under python 2 In python 2, when nested mode was used in a context where the signature contains a var-positional argument, a `TypeError` was raised ; this is now fixed. Fixes [#13](https://github.com/smarie/python-decopatch/issues/13). ### 1.4.2 - Bugfix in flat mode under python 2 In python 2, when flat mode was used in a context where the signature contains a var-positional argument, the arguments were not correctly injected. Fixes [#12](https://github.com/smarie/python-decopatch/issues/12). ### 1.4.1 - Minor default symbols improvement Removed the clunky `Enum` for symbols. Back to a normal class, with a custom `__repr__`. ### 1.4.0 - Minor dependency version update Now relying on `makefun>=1.5.0` where arg names changed a bit. ### 1.3.0 - Predefined disambiguators and init file fix - We now provide predefined disambiguators `with_parenthesis` and `no_parenthesis`. Fixes [#8](https://github.com/smarie/python-decopatch/issues/8). - Fixed `KeyError` when the signature contains `**kwargs`. Fixes [#9](https://github.com/smarie/python-decopatch/issues/9). - Fixed issue when the signature only contains `**kwargs`. Fixes [#10](https://github.com/smarie/python-decopatch/issues/10) - Fixed static checker problem in PyCharm with the symbols (it came back when we moved to an Enum) - Improved exception re-raising in flat mode. - Minor: fixed init file. ### 1.2.1 - Flat and double flat symbols: additional protection and bugfix Default-value symbols `DECORATED`, `WRAPPED`, `F_ARGS` and `F_KWARGS` now have a nicer representation. Fixes [#7](https://github.com/smarie/python-decopatch/issues/7). When a symbol is used in a signature where it can not be safely injected as keyword argument, an `InvalidSignatureError` is now raised. Fixes [#6](https://github.com/smarie/python-decopatch/issues/6). ### 1.2.0 - Dependency update for important fix `makefun>=1.4.0` is now required, as it fixes a major issue: [#5](https://github.com/smarie/python-decopatch/issues/5) ### 1.1.1 - Symbols are not classes anymore Default-value symbols `DECORATED`, `WRAPPED`, `F_ARGS` and `F_KWARGS` are now objects and not classes any more. This prevents IDE to flag the corresponding argument as being misused (not iterable...). Fixes [#4](https://github.com/smarie/python-decopatch/issues/4). ### 1.1.0 - Dependency update The double-flat mode now relies on `@makefun.wraps`, from `makefun>=1.3.0`. Fixes [#3](https://github.com/smarie/python-decopatch/issues/3). Updated documentation accordingly. ### 1.0.0 - Refactoring + New "double-flat" mode + API changes + documentation **API changes:** - Added support for a new "double-flat" mode so that users can create decorators creating *signature-preserving function wrappers* with zero level of nesting. Fixes [#2](https://github.com/smarie/python-decopatch/issues/2). - Generated var-positional name for kw-only methods is now `'*_'` - `can_first_arg_be_ambiguous` parameter removed completely, it was too complex to use. - `callable_or_cls_firstarg_disambiguator` renamed `custom_disambiguator` to be more intuitive - `wraps` argument renamed `flat_mode_decorated_name`. It was too similar to the wording used in functools, and specific to the flat mode only. Fixes [#1](https://github.com/smarie/python-decopatch/issues/1). **Improved behaviour** - Now the flat mode behaves exactly like nested mode concerning signature-related `TypeError`. This is because when you use the flat mode we now generate a nested mode function with a true signature. - Now the exposed decorator uses the `__wrapped__` trick to expose **Major refactoring for code readability:** - Submodules are now consistent and readable. In particular `utils_disambiguation` now provides a clear `disambiguate_call` method, and `util_modes` provides a clear `make_decorator_spec` to handle all the per-mode specificity and always come back to a nested case before going further. - removed a case in the `main` module (var-positional) as it was completely covered by the general case. - Information is now passed as objects through the various functions, using two main classes `SignatureInfo` (for static information) and `DecoratorUsageInfo` (for dynamic/usage information). The information is computed in a lazy way for each to avoid unnecessary signature binding for example. **Major documentation update.** ### 0.5.0 - First version Fully functional with 100+ tests. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/docs/disambiguation.md������������������������������������������������������0000664�0000000�0000000�00000016315�14207357143�0021333�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Disambiguation principles As stated in the [introduction](../index), without language modification (see [proposal](../pep_proposal)) or additional knowledge or source code introspection, python is just **not capable** of detecting that `@say_hello(foo)` is different from `@say_hello` applied to function `foo` without parenthesis. However there are well-known "tricks" to perform a disambiguation that covers most standard cases. `decopatch` is basically a collection of such tricks. Some of them are static, based on your decorator's signature, and some others are dynamic, based on the received arguments. This section explains what happens behind the scenes. The reader is encouraged to get familiar with the basic `decopatch` principles first by reading the [introduction](../index). In particular the two main development styles (nested and flat) are assumed to be understood. All examples below use the "flat" style, but the equivalent "nested" style would lead to the exact same result. ## 1- no-args decorators It is probably quite useless to use `decopatch` if your decorator has no arguments, except if you wish to easily support with- and without- parenthesis usage. This is how you can do it ```python from decopatch import decorator, DECORATED @decorator def replace_with_hello(f=DECORATED): """ Decorator to replace anything with the 'hello' string """ return 'hello' ``` You can of course test that it works fine: ```python @replace_with_hello # no parenthesis def foo(): pass assert foo == 'hello' @replace_with_hello() # with parenthesis def foo(): pass assert foo == 'hello' ``` In this particular case, `decopatch` does not expose a decorator with no arguments as you would expect, but it instead adds a "dummy" var-positional argument named `_`, so that both with- and without- parenthesis usages are supported: ``` >>> help(replace_with_hello) Help on function replace_with_hello in module ...: replace_with_hello(*_) Decorator to replace anything by the 'hello' string. ``` If your users try to input arguments they will get a `TypeError`, except if they provide a single argument that is a callable or a class. In that case we have no means to disambiguate so we prefer to consider that this is a no-parenthesis usage, rather than trying complex disambiguation tricks. After all, your decorator is supposed to have no arguments :) ```python replace_with_hello(1) # TypeError: function 'replace_with_hello' does not accept any argument. replace_with_hello(print) # no error ! ``` See `create_no_args_decorator` in the [source code](https://github.com/smarie/python-decopatch/blob/master/decopatch/main.py) for details. ## 2- keyword-only decorators ### 1+ mandatory argument(s) If your decorator is keyword-only and has **at least one mandatory argument**: ```python @decorator def replace_with(*, replacement, f=DECORATED): """ Decorator to replace anything with the <replacement> object. """ return replacement ``` then everything is very easy for `decopatch`: it can expose your desired signature directly, users have no way to use it in an ambiguous manner, and a successful call will always be a with-parenthesis call. ```python @replace_with(replacement='hello') def foo(): pass assert foo == 'hello' replace_with(1) # TypeError: replace_with() takes 0 positional arguments replace_with(str) # TypeError: replace_with() takes 0 positional arguments ``` See `create_kwonly_decorator` in the [source code](https://github.com/smarie/python-decopatch/blob/master/decopatch/main.py) for details. ### 0 mandatory arguments If your decorator is keyword-only but has **no mandatory argument**: ```python @decorator def replace_with(*, replacement='hello', f=DECORATED): """ Decorator to replace anything with the <replacement> object. Default value is 'hello'. """ return replacement ``` `decopatch` automatically adds a dummy var-positional argument named `_` to the decorator signature so that your users can use it without arguments nor parenthesis: ``` >>>help(replace_with) Help on function replace_with in module ...: replace_with(*_, replacement='hello') Decorator to replace anything with the <replacement> object. ``` Then disambiguations are handled similarly to all other decorators (see below). See `create_kwonly_decorator` in the [source code](https://github.com/smarie/python-decopatch/blob/master/decopatch/main.py) for details. ## 3- General case In the general case, disambiguation is made of two phases: first eliminating cases for which we have no doubts, then handling the remaining ambiguous cases. See `disambiguate_call` in the [source code](https://github.com/smarie/python-decopatch/blob/master/decopatch/utils_disambiguation.py) for details. **a- Eliminating easy cases:** * we look at the number of positional and keyword arguments to eliminate cases that can not be a "no-parenthesis" usage * we look at the value of the argument received to eliminate further: for example, if it is not a callable nor a class it is has to be provided with parenthesis. **b- Handling still-ambiguous cases** If we reach this part, that's because the first argument in the signature is - a callable or a class, - positional (or in python 2, is different from its default value), - and it is the only one (or in python 2, the only one different from its default value) Note: python 2 behaviour should align when this [`funcsigs` issue](https://github.com/testing-cabal/funcsigs/issues/33) is fixed. In that scenario, `decopatch` makes the least worst guess: **this is probably a no-parenthesis call**. Because it is not very probable that your decorator is made to receive a function or a class as first argument. However you can change this default behaviour, by providing additional knowledge. First, you can **explicitly declare that your decorator is a function-only or class-only decorator**, by using `@decorator(is_function_decorator=False)` or `(is_class_decorator=False)`, or by using the shortcut aliases `@class_decorator` or `@function_decorator` respectively. In that case, `decopatch` will know that for example a class received by a function decorator is probably a with-parenthesis first argument. You can also **provide an explicit disambiguation function** (`custom_disambiguator=...`). This function will only be called for ambiguous cases. It should accept a single argument (the first argument's value), and should return either `FirstArgDisambiguation.is_normal_arg` or `FirstArgDisambiguation.is_decorated_target`. It can also return `FirstArgDisambiguation.is_ambiguous` if it can not decide ; in which case an exception will be raised. For your convenience, you can use the predefined disambiguators `custom_disambiguator=with_parenthesis` or `custom_disambiguator=no_parenthesis` if an ambiguous first argument should always be handled as an arg (with parenthesis) or as the decorated target (no parenthesis) respectively. Finally as a last resort scenario you can **enable introspection** (`enable_stack_introspection=True`). This beta feature seems to only work reliably with function decorators, because `inspect.stack` does not provide a reliable way to access the decorator usage source code line when used on a class. Note that it seems to fail when used with `python >=3.8`. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/docs/index.md���������������������������������������������������������������0000664�0000000�0000000�00000025520�14207357143�0017441�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# decopatch *python decorators made easy.* [![Python versions](https://img.shields.io/pypi/pyversions/decopatch.svg)](https://pypi.python.org/pypi/decopatch/) [![Build Status](https://github.com/smarie/python-decopatch/actions/workflows/base.yml/badge.svg)](https://github.com/smarie/python-decopatch/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-decopatch/branch/main/graph/badge.svg)](https://codecov.io/gh/smarie/python-decopatch) [![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-decopatch/) [![PyPI](https://img.shields.io/pypi/v/decopatch.svg)](https://pypi.python.org/pypi/decopatch/) [![Downloads](https://pepy.tech/badge/decopatch)](https://pepy.tech/project/decopatch) [![Downloads per week](https://pepy.tech/badge/decopatch/week)](https://pepy.tech/project/decopatch) [![GitHub stars](https://img.shields.io/github/stars/smarie/python-decopatch.svg)](https://github.com/smarie/python-decopatch/stargazers) Because of a tiny oddity in the python language, writing decorators without help can be a pain because you have to handle the no-parenthesis usage [explicitly](./motivation). `decopatch` provides a simple way to solve this issue so that writing decorators is simple and straightforward. ## Installing ```bash > pip install decopatch ``` ## Usage ### 1- As usual, a.k.a nested mode Let's create a `add_tag` decorator, that will simply add a new attribute on the decorated function: ```python from decopatch import function_decorator @function_decorator def add_tag(tag='hi!'): """ Example decorator to add a 'tag' attribute to a function. :param tag: the 'tag' value to set on the decorated function (default 'hi!). """ def _apply_decorator(f): """ This is the method that will be called when `@add_tag` is used on a function `f`. It should return a replacement for `f`. """ setattr(f, 'tag', tag) return f return _apply_decorator ``` Apart from the new `@function_decorator`, it should look very familiar to those of you who tried to write decorators using the native python mechanisms. **Except** that it works out of the box with and without parenthesis, with and without arguments: ```python @add_tag # no parenthesis def foo(): pass assert foo.tag == 'hi!' @add_tag() # empty parenthesis def foo(): pass assert foo.tag == 'hi!' @add_tag('hello') # with args def foo(): pass assert foo.tag == 'hello' add_tag()(foo) # manual decoration assert foo.tag == 'hi!' ``` Besides, its signature and docstring are preserved: ```python print("%s%s" % (add_tag.__name__, signature(add_tag))) print(help(add_tag)) ``` yields ```bash add_tag(tag='hi!') Help on function add_tag in module decopatch.tests.test_doc: add_tag(tag='hi!') Example decorator to add a 'tag' attribute to a function. :param tag: the 'tag' value to set on the decorated function (default 'hi!). ``` Finally note that `_apply_decorator` *can* return a wrapper, but is not forced to: you are free to return `f`, a wrapper of `f`, or a complete replacement for `f`, not even a function! This is the default python language capability, but we tend to forget it when we use `wrapt` or `decorator` because they are designed for wrappers. ### 2- More compact: flat mode To ease code readability, `decopatch` also supports a *flat* mode: ```python from decopatch import function_decorator, DECORATED @function_decorator def add_tag(tag='hi!', f=DECORATED): """ Example decorator to add a 'tag' attribute to a function. :param tag: the 'tag' value to set on the decorated function (default 'hi!). """ setattr(f, 'tag', tag) return f ``` As you can see, in that mode you can use one less level of nesting. You indicate which argument is the decorated object by using the `DECORATED` default value. But the cool thing is that using this development style does not change the signature that gets exposed to your users: they do not see the `DECORATED` argument, you can check it with `help(add_tag)`! Of course you should not mention it in the docstring. ### 3- Creating function wrappers A very popular use case for decorators is to create signature-preserving function wrappers. The great [`decorator`](https://github.com/micheles/decorator) library in particular, provides tools to solve this problem "all at once" (decorator + signature-preserving wrapper). With `decopatch` and its optional companion [`makefun`](https://smarie.github.io/python-makefun/), each problem is now solved in a dedicated library, because the author believes that these are two completely independent problems. - `decopatch` (this library) focuses on helping you create decorators that nicely handle the without-parenthesis case. You can decorate functions and classes, and your decorator is free to return anything (the same object that was decorated, a wrapper, or another object). - [`makefun`](https://smarie.github.io/python-makefun/) can be used to generate functions with any signature dynamically ; in particular its `@wraps` decorator makes it very easy to create signature-preserving wrappers. It relies on the same tricks than [`decorator`](https://github.com/micheles/decorator) to perform the function generation, but also supports more complex use cases such as signature modification. It can be used anywhere of course, it is not specific to decorators. Both work well together of course: ```python from decopatch import function_decorator, DECORATED from makefun import wraps @function_decorator def say_hello(person="world", f=DECORATED): """ This decorator modifies the decorated function so as to print a greetings message before each execution. :param person: the person name in the print message. Default = "world" """ # (1) create a wrapper of f that will do the print before call @wraps(f) # rely on `makefun` to preserve signature of `f` def new_f(*args, **kwargs): print("hello, %s !" % person) # say hello return f(*args, **kwargs) # execute f # (2) return it as a replacement for `f` return new_f ``` Once again, you can check that all call modes are properly implemented: ```python @say_hello # no parenthesis def foo(): print("<executing foo>") foo() @say_hello() # empty parenthesis def bar(): print("<executing bar>") bar() @say_hello("you") # arg def custom(): print("<executing custom>") custom() # manual decoration def custom2(): print("<executing custom2>") custom2 = say_hello()(custom2) custom2() ``` yields ```bash hello, world ! <executing foo> hello, world ! <executing bar> hello, you ! <executing custom> hello, world ! <executing custom2> ``` As stated previously, you can use any other means to generate your function wrapper at step (1) of this example, such as `functools.wraps`, etc. But beware that not all of them are signature-preserving! #### ...even simpler ? If you **really** want to avoid nesting in the above example (and take the risk of making your code less readable), `decopatch` supports a double-flat mode: ```python from decopatch import function_decorator, WRAPPED, F_ARGS, F_KWARGS @function_decorator def say_hello(person="world", f=WRAPPED, f_args=F_ARGS, f_kwargs=F_KWARGS): """ This decorator modifies the decorated function so as to print a greetings message before execution. :param person: the person name in the print message. Default = "world" """ print("hello, %s !" % person) # say hello return f(*f_args, **f_kwargs) # execute f ``` This syntax is completely equivalent to the one shown previously. You can check it: ```python @say_hello # no parenthesis def add_ints(a, b): return a + b assert add_ints(1, 3) == 4 ``` yields ``` hello, world ! ``` As you can see above, the principles of this syntax are simple: all arguments are decorator arguments, except for the ones with default values `WRAPPED` (the decorated item), `F_ARGS` and `F_KWARGS` (the `*args` and `**kwargs` of each function call). ### 4- Decorating classes You can similarly use `@class_decorator` to create a decorator that works for classes. Both `@function_decorator` and `@class_decorator` are actually user-friendly presets for the more generic `@decorator` function. So if you wish to write a decorator that can be used both for functions and classes, use `@decorator`: ```python from decopatch import decorator @decorator def add_tag(tag='hi!'): """ Example decorator to add a 'tag' attribute to a function or class. :param tag: the 'tag' value to set on the decorated item (default 'hi!). """ def _apply_decorator(o): """ This is the method that will be called when your decorator is used on a class or function `o`. It should return the replacement for this object. """ setattr(o, 'tag', tag) return o return _apply_decorator ``` ## How does it work ? There is no magic here. Without language modification (see [proposal](./pep_proposal)) or additional knowledge or source code introspection, python is just **not capable** of detecting that `@say_hello(foo)` is different from `@say_hello` applied to function `foo`. However in most standard cases there are well-known "tricks" to perform a disambiguation that covers most standard cases. `decopatch` is basically a collection of such tricks. Some of them are static, based on your decorator's signature, and some others are dynamic, based on the received arguments. See [disambiguation details](./disambiguation) for the complete list and for advanced usage. ## See Also - [PEP318](https://www.python.org/dev/peps/pep-0318/) decorators for functions and methods - [PEP3129](https://www.python.org/dev/peps/pep-3129/) decorators for classes - [decorator](https://github.com/micheles/decorator), the reference library for creating decorators in python. I used it a lot before writing `makefun` and `decopatch`, big thanks to [`micheles`](https://github.com/micheles) ! - [tutorial 1](https://realpython.com/primer-on-python-decorators/) - [tutorial 2](https://www.thecodeship.com/patterns/guide-to-python-function-decorators/) - [tutorial 3](http://coderazzi.net/tnotes/python/decoratorsWithoutArguments.html) - [tutorial 4](https://www.saltycrane.com/blog/2010/03/simple-python-decorator-examples/) - [decorator recipes](https://wiki.python.org/moin/PythonDecoratorLibrary) ### 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-decopatch](https://github.com/smarie/python-decopatch) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/docs/long_description.md����������������������������������������������������0000664�0000000�0000000�00000003420�14207357143�0021667�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# decopatch [![Python versions](https://img.shields.io/pypi/pyversions/decopatch.svg)](https://pypi.python.org/pypi/decopatch/) [![Build Status](https://github.com/smarie/python-decopatch/actions/workflows/base.yml/badge.svg)](https://github.com/smarie/python-decopatch/actions/workflows/base.yml) [![Tests Status](https://smarie.github.io/python-decopatch/reports/junit/junit-badge.svg?dummy=8484744)](https://smarie.github.io/python-decopatch/reports/junit/report.html) [![Coverage Status](https://smarie.github.io/python-decopatch/reports/coverage/coverage-badge.svg?dummy=8484744)](https://smarie.github.io/python-decopatch/reports/coverage/index.html) [![codecov](https://codecov.io/gh/smarie/python-decopatch/branch/main/graph/badge.svg)](https://codecov.io/gh/smarie/python-decopatch) [![Flake8 Status](https://smarie.github.io/python-decopatch/reports/flake8/flake8-badge.svg?dummy=8484744)](https://smarie.github.io/python-decopatch/reports/flake8/index.html) [![Documentation](https://img.shields.io/badge/doc-latest-blue.svg)](https://smarie.github.io/python-decopatch/) [![PyPI](https://img.shields.io/pypi/v/decopatch.svg)](https://pypi.python.org/pypi/decopatch/) [![Downloads](https://pepy.tech/badge/decopatch)](https://pepy.tech/project/decopatch) [![Downloads per week](https://pepy.tech/badge/decopatch/week)](https://pepy.tech/project/decopatch) [![GitHub stars](https://img.shields.io/github/stars/smarie/python-decopatch.svg)](https://github.com/smarie/python-decopatch/stargazers) *python decorators made easy.* The documentation for users is available here: [https://smarie.github.io/python-decopatch/](https://smarie.github.io/python-decopatch/) A readme for developers is available here: [https://github.com/smarie/python-decopatch](https://github.com/smarie/python-decopatch) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/docs/motivation.md����������������������������������������������������������0000664�0000000�0000000�00000010233�14207357143�0020516�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Motivation This page explains why I felt frustrated by the current tools that we developers have at our disposal to develop decorators in python, and eventually why I ended-up writing `decopatch`. ## Problem In python, a decorator used without arguments such as ```python @say_hello # (1) no-parenthesis def foo(a, b): pass ``` requires a completely different implementation code than ```python @say_hello() # (2) empty-parenthesis def foo(a, b): pass ``` Indeed (1) requires `say_hello` to directly return a replacement for the decorated object (in that case function `foo`), while (2) requires `say_hello` to return a **function** that returns a replacement for the decorated object ! This is one more level of nesting. If you wish to handle both situations in a robust way, **you end-up having to design some ridiculously complex code**, relying on some well-known "tricks" based either on checking the type or existence of first argument provided. For example: ```python def say_hello(f=None): if f is not None: # this is (1) @say_hello (without parenthesis) # we have to directly return a replacement for f def new_f(...): ... return new_f else: # this is (2) @say_hello() (empty parenthesis) # we have to return a decorator function def _decorate(f): def new_f(...): ... return new_f return _decorate ``` Unfortunately, the 'trick' to use is different for almost every type of decorator signature (var-args, keyword-only, all-optional, ...). So if you change your mind about your API during development time (this often happens, at least to me :)), you end up having to change this useless piece of code several times! ## Solution `decopatch` provides a simple way to solve this issue. It always uses the best "trick", so that you do not have to care, you just implement one case: ```python from decopatch import function_decorator @function_decorator def say_hello(): def _decorate(f): def new_f(...): ... return new_f return _decorate ``` To ease things even more, `decopatch` also supports a *flat* mode: ```python from decopatch import function_decorator, DECORATED @function_decorator def say_hello(f=DECORATED): def new_f(...): ... return new_f ``` In both cases, generated decorators have a proper `help` and `signature`, so users do not see the difference, the choice of mode is a matter of development style. ## Why something new ? As opposed to the great [`decorator`](https://github.com/micheles/decorator) and [`wrapt`](https://wrapt.readthedocs.io/) libraries, `decopatch` does not try **at the same time** to help you create decorators **and** (signature-preserving) function wrappers. In my opinion creating function wrappers is a completely independent topic, you can wish to do it in with a decorator OR without. Nevertheless since it is an important use case, the documentation shows [how to do it](). If you're interested in this topic, see [`makefun`](https://smarie.github.io/python-makefun/), my fork of `decorator`'s core engine supporting additional use cases such as signature modification. Also, note that (at the time of writing so with `decorator` 4.3.2 and `wrapt` 1.11.1): - none of `wrapt` and `decorator` preserve the exposed decorator's signature: they both rely on an extra optional argument (`func=None` and `wrapped=None` respectively). - `decorator` relies on the `__wrapped__` special field to trick the `signature` module about the true signature of the decorator. You can check it by deleting this field on your decorator, and query its signature again: you will see the true signature of your decorator (the one seen by the python interpreter when calling the decorator). - none of `decorator` and `wrapt` handle the no-parenthesis case natively. `wrapt` [explains which lines of codes you have to enter](https://wrapt.readthedocs.io/en/latest/decorators.html#decorators-with-optional-arguments) while with `decorator` you have to write the parenthesis. ## Could we solve this directly in the python language instead ? That would be great ! See my [PEP proposal draft](../pep_proposal). ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/docs/pep_proposal.md��������������������������������������������������������0000664�0000000�0000000�00000010623�14207357143�0021033�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# PEP proposal draft As already stated in this documentation, there is as of python 3.7, absolutely no way to detect that the usage of `@say_hello` in: ```python @say_hello(foo) def bar(): pass ``` is different from ```python @say_hello def foo(): pass ``` As we saw in the [disambiguation principles](../disambiguation/#3-general-case), a library can disambiguate only if the decorator developer provides additional knowledge. Below are a few ideas that would fix this problem once and for all (hopefully!) directly in the python language instead. Feedback welcome ! ## 1- breaking backwards compatibility The easiest way to fix the no-parenthesis case directly in the language would be to automatically redirect **all** decorator usage without parenthesis, to empty-parenthesis. So if you have a `set_hello_tag(tag='world')` decorator, if you use it without parenthesis: ```python @set_hello_tag def foo(): pass ``` the python language interpreter would interprete it exactly the same way than ```python @set_hello_tag() def foo(): pass ``` In other words, using a decorator with the `@` syntax and without parenthesis would simply be an alias for the same usage with empty parenthesis. Implementation would therefore be extremely easy: for all decorators, you would always code it in a "nested" way: ```python def set_hello_tag(tag='world'): def decorate(f): setattr(f, 'hello', tag) # set a hello=<tag> tag on the decorated f return f return decorate ``` **Drawbacks**: this change in the python interpreter behaviour would obviously break compatibility for legacy decorators that did not support arguments at all, such as this one: ```python def set_helloworld_tag(f): setattr(f, 'hello', 'world') # set a hello=world tag on the decorated f return f ``` ## 2- preserving backwards compatibility If we need to preserve backwards compatibility, then we need to make the new mechanism optional, so that developers explicitly choose to use it. My suggestion would be to introduce a new `@decorator_factory` decorator in the `stdlib`, that developers would use to declare that they are ok with redirecting all no-parenthesis usages to with-empty-parenthesis usages. Note that I use the term "factory" because [some users](https://stackoverflow.com/questions/28693930/when-to-use-decorator-and-decorator-factory) use it to distinguish between the no-argument ones (decorators) and the with-argument ones (decorator factories). This is how you would create a `set_hello_tag` decorator: ```python @decorator_factory def set_hello_tag(tag='world'): def decorate(f): setattr(f, 'hello', tag) # set a hello tag on the decorated f return f return decorate ``` The explicit `@decorator_factory` annotation would make the interpreter/stdlib redirect all occurences of `@set_hello_tag` (without parenthesis) into explicit `@set_hello_tag()` (with empty parenthesis). **Notes:** - if this is a too low-level feature it might require a dedicated language symbol instead of a "normal" decorator ; but it seems overkill - it would be better if we can avoid creating a new language element. - alternatively or in addition, the python `stdlib` could provide a method that would return `True` if and only if a given frame is a no-parenthesis decorator call. This method, for example named `inspect.is_decorator_call(frame=None)`, could then be used by the various helper libraries, including `decopatch`. **Follow-up** After proposing the above to the python-ideas mailing list, it seems that people were not as interested as I thought they would be. I therefore proposed a very minimal feature in [the python bug tracker](https://bugs.python.org/issue36553) (the `inspect.is_decorator_call(frame)` option discussed above). At least with this, everyone would be able to solve the issue easily himself, like this: ```python from inspect import is_decorator_call def set_hello_tag(tag='world'): if is_decorator_call(): # called without parenthesis! # the decorated object is `tag` return set_hello_tag()(tag) # note that `is_decorator_call` should not return True for this call else: def decorate(f): setattr(f, 'hello', tag) # set a hello tag on the decorated f return f return decorate ``` Note that `is_decorator_call` should **not** return `True` in **nested** frames, just for the immediate frame in a decorator application. �������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/docs/usage_details.md�������������������������������������������������������0000664�0000000�0000000�00000032140�14207357143�0021137�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Usage details **WARNING: THIS IS AN OLD PAGE WITH OUTDATED INFORMATION ! PLEASE IGNORE IT !** ### a- Flat mode In this mode the decorator is implemented as a function, that should return the replacement for the decorated item. To create a decorator you have to do two things: - use `@function_decorator`, `@class_decorator` or `@decorator` on your implementation function - declare which variable represents the injected decorated item by using the `DECORATED` keyword as its default value. Note: if you can not or do not want to add the `DECORATED` default value, you can specify the variable name explicitly with `decorated=<argname>`. #### *1- Simple with mandatory arg* Let's create a simple `@add_tag` decorator that adds a tag on the decorated function: ```python from decopatch import function_decorator, DECORATED @function_decorator def add_tag(tag, f=DECORATED): """ This decorator adds the 'my_tag' tag on the decorated function, with the value provided as argument :param tag: the tag value to set :param f: represents the decorated item. Automatically injected. :return: """ setattr(f, 'my_tag', tag) return f ``` You can test that your new `@add_tag` decorator works: ```python @add_tag('hello') def foo(): return # let's check that the `foo` function has been correctly decorated assert foo.my_tag == 'hello' ``` And also that using your decorator without argument raises an error as expected: ```python @add_tag def foo(): return ``` yields `TypeError: add_tag() missing 1 required positional argument: 'tag'`. Finally, note that calling the decorator with a callable as first argument is even correctly handled: ```python @add_tag(print) def foo(): return assert foo.my_tag == print # works ! ``` #### *2- Same with all-optional args* Let's modify the above example so that the argument is optional: ```python @function_decorator def add_tag(tag='tag!', f=DECORATED): setattr(f, 'my_tag', tag) return f ``` You can check that everything works as expected: ```python @add_tag('hello') # normal arg def foo(): return assert foo.my_tag == 'hello' @add_tag(tag='hello') # normal kwarg def foo(): return assert foo.my_tag == 'hello' @add_tag # no parenthesis def foo(): return assert foo.my_tag == 'tag!' @add_tag() # empty parenthesis def foo(): return assert foo.my_tag == 'tag!' @add_tag(print) # callable as first arg def foo(): return assert foo.my_tag == print @add_tag(tag=print) # callable as first kwarg def foo(): return assert foo.my_tag == print ``` #### *3- Function wrapper creator* In real-world applications you often wish to not only modify the decorated item, but to **replace** it with something else. A typical use case is the creation of a **function wrapper**, for example to add behaviours to a function when decorating it. The great `wrapt` and `decorator` libraries have been designed mostly to cover this purpose, but they blend it quite tightly to the decorator creation. Below we show that the same result can be obtained by combining two distinct libraries: `decopatch` to create the decorator, and the library of your choice for the signature-preserving wrapper. In this example we use `makefun`. Let's create a `@say_hello` decorator, that prints a message to stdout before each call to the decorated function. ```python from decopatch import function_decorator, DECORATED from makefun import with_signature @function_decorator def say_hello(person="world", f=DECORATED): """ This decorator modifies the decorated function so that a nice hello message is printed before the call. :param person: the person name in the print message. Default = "world" :param f: represents the decorated item. Automatically injected. :return: a modified version of `f` that will print a msg before executing """ # create a wrapper of f that will do the print before call # we rely on `makefun.with_signature` to preserve signature @with_signature(f) def new_f(*args, **kwargs): nonlocal person print("hello, %s !" % person) # say hello return f(*args, **kwargs) # call f # return the new function return new_f ``` Once again, you can check that all call modes are properly implemented: ```python @say_hello def foo(): print("<executing foo>") foo() @say_hello() def bar(): print("<executing bar>") bar() @say_hello("you") def custom(): print("<executing custom>") custom() ``` yields ```bash hello, world ! <executing foo> hello, world ! <executing bar> hello, you ! <executing custom> ``` !!! note "nonlocal in python 2" In python 2 the `nonlocal` keyword from [PEP3104](https://www.python.org/dev/peps/pep-3104/) is not available. See [this workaround](https://stackoverflow.com/a/16032631/7262247) #### *4- Class decorator* You can similarly use `@class_decorator` to create a decorator that works for classes. #### *5- Class+Function decorator* Both `@function_decorator` and `@class_decorator` are actually user-friendly presets for the more generic `@decorator`. If you wish to write a decorator that can be used both for functions and classes, you can use it. ### b- Nested mode In *nested* mode you write your decorator's signature thinking about what the **user** will have to enter. So for example if you wish to create `@say_hello(person)` you will define `def say_hello(person)`. In that case your function has to return a function. In other words, *nested* mode is equivalent to how you write python decorators with arguments today, except that you do not have to write anything special to handle the case where your decorator is used without parenthesis. It is silently redirected to the case where it is used with parenthesis. To write decorators in this mode, you only have to decorate them with `@function_decorator`, `@class_decorator` or `@decorator`. #### *1- Simple with mandatory arg* We can reproduce the same example than above, in this alternate style: ```python @function_decorator def add_tag(tag): """ This decorator adds the 'my_tag' tag on the decorated function, with the value provided as argument :param tag: the tag value to set :return: """ def replace_f(f): setattr(f, 'my_tag', tag) return f return replace_f ``` You can test that your new `@add_tag` decorator works: ```python @add_tag('hello') def foo(): return # let's check that the `foo` function has been correctly decorated assert foo.my_tag == 'hello' ``` ### c- Behind the scenes When you use `@function_decorator` or the like, your function is dynamically replaced with another one with the same signature (if you're in *nested* mode) or where the `f` parameter is removed (if you're in *flat* mode). You can see it by using `help` on your function, or by looking at its signature: ```python help(say_hello) from inspect import signature print("Signature: %s" % signature(say_hello)) ``` yields ```bash Help on function say_hello in module my_module: say_hello(person='world') This decorator modifies the decorated function so that a nice hello message is printed before the call. :param person: the person name in the print message. Default = "world" :return: Signature: (person='world') ``` ### c- Advanced topics 1 mandatory either ```python InvalidMandatoryArgError: function 'add_tag' requires a mandatory argument 'tag'. Provided value '<function foo at 0x00000160858ED510>' does not pass its validation criteria ``` Note that this error is a dedicated subclass of `TypeError`, and not the native `TypeError` that python raises when a function is called without a mandatory argument. This is mostly for testability reasons, to make sure that the library worked correctly. By default **you cannot use a callable as first mandatory argument**: ```python @add_tag(print) def foo(): return ``` raises ```python InvalidMandatoryArgError: function 'add_tag' requires a mandatory argument 'tag'. Provided value '<built-in function print>' does not pass its validation criteria ``` Indeed there is absolutely no way in the python language to disambiguate this case with the previous one without user-provided disambiguation rules. Since we wanted to avoid silent incorrect behaviour in the previous (no-parenthesis) usage, then the consequence is that we have an error in this usage that seems legitimate. **It is very easy to fix this**: if you wish to accept callables/classes as first mandatory argument, the simplest thing to do is to **force keyword usage** (adding a star as first argument): ```python @function_decorator def add_tag(*, tag, f=DECORATED): """ We use the * in the signature to disambiguate """ setattr(f, 'my_tag', tag) return f ``` Now your decorator will be entirely not ambiguous, users will be able to pass callables to it while still getting an error in case they use it without arguments. ```python @add_tag(tag=print) # >> disambiguated: now works correctly def foo(): return assert foo.my_tag == print @add_tag # >> disambiguated: raises the standard TypeError now def foo(): return ``` Note: if you do not want to use this trick, there are other, more cumbersome ways to perform the disambiguation. See [this section on first argument disambiguation](). 1 optional Let's modify the previous decorator so that its `tag` argument is optional: ```python @function_decorator def add_tag(tag='tag!', f=DECORATED): setattr(f, 'my_tag', tag) return f ``` You see that `decopath` does not accept it: ```python AmbiguousDecoratorDefinitionError: This decorator is ambiguous because it has only optional arguments. Please provide an explicit protection. ``` Indeed this is a very ambiguous decorator: the decorator - can be used without arguments: `@add_tag` or `@add_tag()` - but it can also be used with arguments: `@add_tag('hello')`. In particular it can be used with a callable as first argument such as in `@add_tag(print)` The python language is not able to see the difference between using `@add_tag` to decorate a function named `print`, and `@add_tag(print)` applied to decorate another function: ```python # the following two usages are programmatically undetectable # except if we cheat and look at the source code @add_tag def print(): return @add_tag(print) def another(): return ``` , and therefore we ask you to explicitly protect your decorator. Once again you can use the "keyword-only" trick, or if you have a good reason not to do it, see [this section on first argument disambiguation](). Wrapper Note that this time usage without arguments is permitted, as long as the value provided for the first argument is not a callable - as that would be ambiguous. In very rare cases, you want to create a decorator - that accepts to be called with a single argument (i.e. it has a single mandatory argument, or only optional ones) - where the first argument in the signature, except the `DECORATED` one, is a callable or a class. For example the following dummy decorator will replace the decorated function with its argument: ```python @decorator def replace_with(g, f=DECORATED): """replace the decorated function with its argument""" return g ``` If you try to use it ```python def foo(): pass @replace_with(foo) def bar(): pass ``` Surprisingly it fails ! ```bash TypeError: replace_with() missing 1 required positional argument: 'g' ``` This is because there is **absolutely no way in the python language** (if you know one, let me know !) to disambiguate the above case from the "no-arg" usage below: ```python @replace_with def bar_no_arg(): pass ``` Hopefully there are many workarounds if you use `decopatch`. The easiest ones consist in changing your decorator's signature: - so that the first argument is keyword-only. **This is the most reliable thing to do**. For this, simply add a `*` before your first argument: ```python @decorator def replace_with(*, g, f=DECORATED): # ... ``` - or if the first argument is optional, so that the callable/class is not the first in the list. In this case you have to either declare a type hint or perform manual validation in the code: ```python @decorator def replace_with(a: int=None, g=None, f=DECORATED): # ... # or @decorator def replace_with(a=None, g=None, f=DECORATED): if not isinstance(a, int): raise TypeError() # ... ``` - or if the first argument is mandatory, so that it is not alone (at least two mandatory arguments) ```python @decorator def replace_with(g, a, f=DECORATED): # ... ``` If you can not afford a signature change, there are two other ways to work around this problem: - change the default detector for first argument disambiguation. By default if a single argument is received, it will be assumed to be the true first arg (and not the decorator's target), if it is not a callable nor a class. If you are sure that a function of a certain type, or a class with certain caracteristics, can only be the argument and not the decorated object, then you can override this default logic. For example we could use `@decorator(first_arg_disambiguator=lambda g: g in {foo})` in the above code. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/mkdocs.yml������������������������������������������������������������������0000664�0000000�0000000�00000001600�14207357143�0017054�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������site_name: decopatch # site_description: 'A short description of my project' repo_url: https://github.com/smarie/python-decopatch # 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 - Motivation: motivation.md - Disambiguation principes: disambiguation.md # - Usage details: usage_details.md - PEP proposal: pep_proposal.md - Changelog: changelog.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-decopatch-1.4.10/noxfile-requirements.txt����������������������������������������������������0000664�0000000�0000000�00000000131�14207357143�0021775�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������nox toml makefun setuptools_scm # used in 'release' keyring # used in 'release' ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/noxfile.py������������������������������������������������������������������0000664�0000000�0000000�00000030547�14207357143�0017103�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from itertools import product from json import dumps 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, PY35, PY38, PY39, PY310, power_session, rm_folder, rm_file, PowerSession # noqa pkg_name = "decopatch" gh_org = "smarie" gh_repo = "python-decopatch" ENVS = { # python 3.10 is not available on conda yet # 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"}}, PY35: {"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" } # set the default activated sessions, minimal for CI nox.options.sessions = ["tests", "flake8"] # , "docs", "gh_pages" 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 = "conda" # 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" @power_session(envs=ENVS, logsdir=Folders.runlogs) def tests(session: PowerSession, 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 session.install_reqs(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.run2("python ci_tools/check_python_version.py %s" % 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.run2("pip install . --no-deps") # session.install(".", "--no-deps") # simple: pytest only session.run2("python -m pytest --cache-clear -v tests/") else: # install self in "develop" mode so that coverage can be measured session.run2("pip install -e . --no-deps") # coverage + junit html reports + badge generation session.install_reqs(phase="coverage", phase_reqs=["coverage", "pytest-html", "genbadge[tests,coverage]"], versions_dct=pkg_specs) # --coverage + junit html reports session.run2("coverage run --source src/{pkg_name} " "-m pytest --cache-clear --junitxml={test_xml} --html={test_html} -v tests/" "".format(pkg_name=pkg_name, test_xml=Folders.test_xml, test_html=Folders.test_html)) session.run2("coverage report") session.run2("coverage xml -o {covxml}".format(covxml=Folders.coverage_xml)) session.run2("coverage html -d {dst}".format(dst=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.run2("genbadge tests -i %s -o %s -t 100" % (Folders.test_xml, Folders.test_badge)) session.run2("genbadge coverage -i %s -o %s" % (Folders.coverage_xml, Folders.coverage_badge)) @power_session(python=PY38, logsdir=Folders.runlogs) def flake8(session: PowerSession): """Launch flake8 qualimetry.""" session.install("-r", str(Folders.ci_tools / "flake8-requirements.txt")) session.install("genbadge[flake8]") session.run2("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.run2("genbadge flake8 -i %s -o %s" % (Folders.flake8_intermediate_file, Folders.flake8_badge)) rm_file(Folders.flake8_intermediate_file) @power_session(python=[PY37]) def docs(session: PowerSession): """Generates the doc and serves it on a local http server. Pass '-- build' to build statically instead.""" session.install_reqs(phase="docs", phase_reqs=["mkdocs-material", "mkdocs", "pymdown-extensions", "pygments"]) if session.posargs: # use posargs instead of "serve" session.run2("mkdocs %s" % " ".join(session.posargs)) else: session.run2("mkdocs serve") @power_session(python=[PY37]) def publish(session: PowerSession): """Deploy the docs+reports on github pages. Note: this rebuilds the docs""" session.install_reqs(phase="mkdocs", phase_reqs=["mkdocs-material", "mkdocs", "pymdown-extensions", "pygments"]) # possibly rebuild the docs in a static way (mkdocs serve does not build locally) session.run2("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.run2("mkdocs gh-deploy") # publish the coverage - now in github actions only # session.install_reqs(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)) @power_session(python=[PY37]) def release(session: PowerSession): """Create a release on github corresponding to the latest tag""" # 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 session.install_reqs(phase="setup.py#dist", phase_reqs=["setuptools_scm"]) rm_folder(Folders.dist) session.run2("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 session.install_reqs(phase="PyPi", phase_reqs=["twine"]) session.run2("twine upload dist/* -u smarie") # -r testpypi # create the github release session.install_reqs(phase="release", phase_reqs=["click", "PyGithub"]) session.run2("python ci_tools/github_release.py -s {gh_token} " "--repo-slug {gh_org}/{gh_repo} -cf ./docs/changelog.md " "-d https://{gh_org}.github.io/{gh_repo}/changelog {tag}" "".format(gh_token=gh_token, gh_org=gh_org, gh_repo=gh_repo, tag=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 # get the desired base session to generate the list for if len(session.posargs) != 1: raise ValueError("This session has a mandatory argument: <base_session_name>") session_func = globals()[session.posargs[0]] # list all sessions for this base session try: session_func.parametrize except AttributeError: sessions_list = ["%s-%s" % (session_func.__name__, py) for py in session_func.python] else: sessions_list = ["%s-%s(%s)" % (session_func.__name__, py, param) for py, param in product(session_func.python, session_func.parametrize)] # 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(dumps(sessions_list)) # if __name__ == '__main__': # # allow this file to be executable for easy debugging in any IDE # nox.run(globals()) ���������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/pyproject.toml��������������������������������������������������������������0000664�0000000�0000000�00000001105�14207357143�0017765�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 [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" ] # pytest: not with conda ! does not work in old python 2.7 and 3.5 �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/setup.cfg�������������������������������������������������������������������0000664�0000000�0000000�00000011402�14207357143�0016673�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 = decopatch description = Create decorators easily in python. description_file = README.md license = BSD 3-Clause long_description = file: docs/long_description.md long_description_content_type=text/markdown keywords = decorate decorator function class parenthesis generate generation define definition signature args wrapper wraps auto easy preserve author = Sylvain MARIE <sylvain.marie@se.com> maintainer = Sylvain MARIE <sylvain.marie@se.com> url = https://github.com/smarie/python-decopatch # download_url = https://github.com/smarie/python-decopatch/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 [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 pytest-runner install_requires = makefun>=1.5.0 # note: do not use double quotes in these, this triggers a weird bug in PyCharm in debug mode only funcsigs;python_version<'3.3' enum34;python_version<'3.4' tests_require = pytest pytest_cases # 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 = decopatch # command_line = -m pytest --junitxml="reports/pytest_reports/pytest.xml" --html="reports/pytest_reports/pytest.html" -v decopatch/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\-decopatch>\n\#\n\#\s+License:\s3\-clause\sBSD,\s<https://github\.com/smarie/python\-decopatch/blob/master/LICENSE> exclude = .git .github .nox .pytest_cache ci_tools docs tests noxfile.py setup.py */_version.py ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/setup.py��������������������������������������������������������������������0000664�0000000�0000000�00000002653�14207357143�0016574�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-decopatch" 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/decopatch/_version.py" }, # we can't put `use_scm_version` in setup.cfg yet unfortunately ) �������������������������������������������������������������������������������������python-decopatch-1.4.10/src/������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14207357143�0015643�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/src/decopatch/��������������������������������������������������������������0000775�0000000�0000000�00000000000�14207357143�0017575�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/src/decopatch/__init__.py���������������������������������������������������0000664�0000000�0000000�00000002274�14207357143�0021713�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from decopatch.utils_modes import DECORATED, WRAPPED, F_ARGS, F_KWARGS, InvalidSignatureError from decopatch.utils_disambiguation import FirstArgDisambiguation, with_parenthesis, no_parenthesis, is_decorator_call from decopatch.utils_calls import AmbiguousFirstArgumentTypeError, InvalidMandatoryArgError from decopatch.main import function_decorator, class_decorator, decorator 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)) __all__ = [ '__version__', # submodules 'main', 'utils_disambiguation', 'utils_modes', 'utils_calls', # symbols 'DECORATED', 'WRAPPED', 'F_ARGS', 'F_KWARGS', 'InvalidSignatureError', 'FirstArgDisambiguation', 'with_parenthesis', 'no_parenthesis', 'AmbiguousFirstArgumentTypeError', 'InvalidMandatoryArgError', 'function_decorator', 'class_decorator', 'decorator', 'is_decorator_call' ] ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/src/decopatch/main.py�������������������������������������������������������0000664�0000000�0000000�00000041321�14207357143�0021074�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from makefun import with_signature, add_signature_parameters from decopatch.utils_modes import SignatureInfo, make_decorator_spec from decopatch.utils_disambiguation import create_single_arg_callable_or_class_disambiguator, disambiguate_call, \ DecoratorUsageInfo, can_arg_be_a_decorator_target from decopatch.utils_calls import with_parenthesis_usage, no_parenthesis_usage, call_in_appropriate_mode try: # python 3.3+ from inspect import signature, Parameter except ImportError: from funcsigs import signature, Parameter try: # python 3.5+ from typing import Callable, Any, Optional except ImportError: pass def function_decorator(enable_stack_introspection=False, # type: bool custom_disambiguator=None, # type: Callable[[Any], FirstArgDisambiguation] flat_mode_decorated_name=None, # type: Optional[str] ): """ A decorator to create function decorators. Equivalent to decorator(is_function_decorator=True, is_class_decorator=False) :param enable_stack_introspection: :param custom_disambiguator: :param flat_mode_decorated_name: :return: """ if callable(enable_stack_introspection): # no-parenthesis call f = enable_stack_introspection return decorator(is_function_decorator=True, is_class_decorator=False)(f) else: return decorator(is_function_decorator=True, is_class_decorator=False, enable_stack_introspection=enable_stack_introspection, custom_disambiguator=custom_disambiguator, flat_mode_decorated_name=flat_mode_decorated_name) def class_decorator(enable_stack_introspection=False, # type: bool custom_disambiguator=None, # type: Callable[[Any], FirstArgDisambiguation] flat_mode_decorated_name=None, # type: Optional[str] ): """ A decorator to create class decorators Equivalent to decorator(is_function_decorator=False, is_class_decorator=True) :param enable_stack_introspection: :param custom_disambiguator: :param flat_mode_decorated_name: :return: """ if callable(enable_stack_introspection): # no-parenthesis call f = enable_stack_introspection return decorator(is_function_decorator=False, is_class_decorator=True)(f) else: return decorator(is_function_decorator=False, is_class_decorator=True, enable_stack_introspection=enable_stack_introspection, custom_disambiguator=custom_disambiguator, flat_mode_decorated_name=flat_mode_decorated_name) def decorator(is_function_decorator=True, # type: bool is_class_decorator=True, # type: bool enable_stack_introspection=False, # type: bool custom_disambiguator=None, # type: Callable[[Any], FirstArgDisambiguation] use_signature_trick=True, # type: bool flat_mode_decorated_name=None, # type: str ): """ A decorator to create decorators. It support two main modes: "nested", and "flat". In "flat" mode your implementation is flat: ```python def my_decorator(a, b, f=DECORATED): # ... return <replacement for f> ``` For this mode to be automatically detected, your implementation has to have an argument with default value `DECORATED`, or a non-None `decorated` argument name should be provided. This argument will be injected with the decorated target when your decorator is used. Otherwise the "nested" mode is activated. In this mode your implementation is nested, as usual in python: ```python def my_decorator(a, b): def replace_f(f): # ... return <replacement for f> return replace_f ``` In both modes, because python language does not redirect no-parenthesis usages (@my_decorator) to no-args usages (@my_decorator()), `decopatch` tries to disambiguate automatically the type of call. See documentation for details. Finally you can use this function to directly create a "signature preserving function wrapper" decorator. This mode is called "double flat" because it saves you from 2 levels of nesting. For this, use the `WRAPPED` default value instead of `DECORATED`, and include two arguments with default values `F_ARGS` and `F_KWARGS`: ```python @function_decorator def say_hello(person="world", f=WRAPPED, f_args=F_ARGS, f_kwargs=F_KWARGS): ''' This decorator wraps the decorated function so that a nice hello message is printed before each call. :param person: the person name in the print message. Default = "world" ''' print("hello, %s !" % person) # say hello return f(*f_args, **f_kwargs) # call f ``` :param is_function_decorator: :param is_class_decorator: :param enable_stack_introspection: :param custom_disambiguator: :param use_signature_trick: if set to `True`, generated decorators will have a generic signature but the `help` and `signature` modules will still think that they have the specific signature, because by default they follow the `__wrapped__` attribute if it is set. See https://docs.python.org/3/library/inspect.html#inspect.signature for details. :param flat_mode_decorated_name: :return: """ if callable(is_function_decorator): # called without argument: the first argument is actually the decorated function f = is_function_decorator return create_decorator(f) else: # called with argument. Return a decorator function def _apply_on(f): return create_decorator(f, is_function_decorator=is_function_decorator, is_class_decorator=is_class_decorator, enable_stack_introspection=enable_stack_introspection, custom_disambiguator=custom_disambiguator, flat_mode_decorated_name=flat_mode_decorated_name, use_signature_trick=use_signature_trick) return _apply_on def create_decorator(impl_function, is_function_decorator=True, # type: bool is_class_decorator=True, # type: bool enable_stack_introspection=False, # type: bool custom_disambiguator=None, # type: Callable[[Any], FirstArgDisambiguation] use_signature_trick=True, # type: bool flat_mode_decorated_name=None, # type: Optional[str] ): """ Main function to create a decorator implemented with the `decorator_function` implementation. :param impl_function: :param is_function_decorator: :param is_class_decorator: :param enable_stack_introspection: :param custom_disambiguator: :param use_signature_trick: :param flat_mode_decorated_name: :return: """ # input checks if not is_function_decorator and not is_class_decorator: raise ValueError("At least one of `is_function_decorator` and `is_class_decorator` must be True") # (1) --- Detect mode and prepare signature to generate -------- sig_info, f_for_metadata, nested_impl_function = make_decorator_spec(impl_function, flat_mode_decorated_name) sig_info.use_signature_trick = use_signature_trick # (2) --- Generate according to the situation-------- # check if the resulting function has any parameter at all if len(sig_info.exposed_signature.parameters) == 0: # (A) no argument at all. Special handling. return create_no_args_decorator(nested_impl_function, function_for_metadata=f_for_metadata) else: # (B) general case: at least one argument # if the decorator has at least 1 mandatory argument, we allow it to be created but its default behaviour # is to raise errors only on ambiguous cases. Usually ambiguous cases are rare (not nominal cases) disambiguator = create_single_arg_callable_or_class_disambiguator(nested_impl_function, is_function_decorator, is_class_decorator, custom_disambiguator, enable_stack_introspection, signature_knowledge=sig_info) if sig_info.is_first_arg_keyword_only: # in this case the decorator *can* be used without arguments but *cannot* with one positional argument, # which will happen in the no-parenthesis case. We have to modify the signature to allow no-parenthesis return create_kwonly_decorator(sig_info, nested_impl_function, disambiguator, function_for_metadata=f_for_metadata) # general case return create_general_case_decorator(sig_info, nested_impl_function, disambiguator, function_for_metadata=f_for_metadata) def create_no_args_decorator(decorator_function, function_for_metadata=None, ): """ Utility method to create a decorator that has no arguments at all and is implemented by `decorator_function`, in implementation-first mode or usage-first mode. The created decorator is a function with var-args. When called it checks the length (0=called with parenthesis, 1=called without, 2=error). Note: we prefer to use this var-arg signature rather than a "(_=None)" signature, because it is more readable for the decorator's help. :param decorator_function: :param function_for_metadata: an alternate function to use for the documentation and module metadata of the generated function :return: """ if function_for_metadata is None: function_for_metadata = decorator_function @with_signature(None, func_name=function_for_metadata.__name__, doc=function_for_metadata.__doc__, module_name=function_for_metadata.__module__) def new_decorator(*_): """ Code for your decorator, generated by decopatch to handle the case when it is called without parenthesis """ if len(_) == 0: # called with no args BUT parenthesis: @foo_decorator(). return with_parenthesis_usage(decorator_function, *_) elif len(_) == 1: first_arg_value = _[0] if can_arg_be_a_decorator_target(first_arg_value): # called with no arg NOR parenthesis: @foo_decorator return no_parenthesis_usage(decorator_function, first_arg_value) # more than 1 argument or non-decorable argument: not possible raise TypeError("Decorator function '%s' does not accept any argument." "" % decorator_function.__name__) return new_decorator _GENERATED_VARPOS_NAME = '_' def create_kwonly_decorator(sig_info, # type: SignatureInfo decorator_function, disambiguator, function_for_metadata, ): """ Utility method to create a decorator that has only keyword arguments and is implemented by `decorator_function`, in implementation-first mode or usage-first mode. When the decorator to create has a mandatory argument, it is exposed "as-is" because it is directly protected. Otherwise (if all arguments are optional and keyword-only), we modify the created decorator's signature to add a leading var-args, so that users will be able to call the decorator without parenthesis. When called it checks the length of the var-positional received: - 0 positional=called with parenthesis, - 1 and the positional argument is not a callable/class : called with parenthesis - 1 and the positional argument is a callable/class: disambiguation is required to know if this is without parenthesis or with positional arg - 2 positional=error). Note: we prefer to use this var-arg signature rather than a "(_=None)" signature, because it is more readable for the decorator's help. :param sig_info: :param decorator_function: :param function_for_metadata: an alternate function to use for the documentation and module metadata of the generated function :return: """ if sig_info.is_first_arg_mandatory: # The first argument is mandatory AND keyword. So we do not need to change the signature to be fully protected # indeed python will automatically raise a `TypeError` when users will use this decorator without parenthesis # or with positional arguments. @with_signature(sig_info.exposed_signature, func_name=function_for_metadata.__name__, doc=function_for_metadata.__doc__, modulename=function_for_metadata.__module__) def new_decorator(*no_args, **kwargs): """ Code for your decorator, generated by decopatch to handle the case when it is called without parenthesis """ # this is a parenthesis call, because otherwise a `TypeError` would already have been raised by python. return with_parenthesis_usage(decorator_function, *no_args, **kwargs) return new_decorator elif sig_info.use_signature_trick: # no need to modify the signature, we will expose *args, **kwargs pass else: # modify the signature to add a var-positional first gen_varpos_param = Parameter(_GENERATED_VARPOS_NAME, kind=Parameter.VAR_POSITIONAL) sig_info.exposed_signature = add_signature_parameters(sig_info.exposed_signature, first=[gen_varpos_param]) # we can fallback to the same case than varpositional return create_general_case_decorator(sig_info, decorator_function, disambiguator, function_for_metadata=function_for_metadata) def create_general_case_decorator(sig_info, # type: SignatureInfo impl_function, disambiguator, function_for_metadata, ): """ This method supports both with-trick and without-trick :param sig_info: :param impl_function: :param disambiguator: :param function_for_metadata: an alternate function to use for the documentation and module metadata of the generated function :return: """ # Note: since we expose a decorator with a preserved signature and not (*args, **kwargs) # we lose the information about the number of arguments *actually* provided. # `@with_signature` will send us all arguments, including the defaults (because it has no way to # determine what was actually provided by the user and what is just the default). So in this decorator we may # receive several kwargs # - even if user did not provide them # - and even if user provided them as positional !! (except for var-positional and fuutre positional-only args) @with_signature(None if sig_info.use_signature_trick else sig_info.exposed_signature, func_name=function_for_metadata.__name__, doc=function_for_metadata.__doc__, module_name=function_for_metadata.__module__) def new_decorator(*args, **kwargs): """ Code for your decorator, generated by decopatch to handle the case when it is called without parenthesis """ # disambiguate dk = DecoratorUsageInfo(sig_info, args, kwargs) disambiguation_result = disambiguate_call(dk, disambiguator) # call return call_in_appropriate_mode(impl_function, dk, disambiguation_result) # trick to declare that our true signature is different than our actual one if sig_info.use_signature_trick: # thanks to setting this field, python help() and signature() will be tricked without compromising the # actual code signature (so, no dynamic function creation in @with_signature above). # Indeed by default they follow the `__wrapped__` attribute if it is set. See # https://docs.python.org/3/library/inspect.html#inspect.signature for details. new_decorator.__wrapped__ = impl_function return new_decorator ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/src/decopatch/main.pyi������������������������������������������������������0000664�0000000�0000000�00000005056�14207357143�0021252�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import Any, Callable, Optional, Protocol, TypeVar, overload from decopatch.utils_disambiguation import FirstArgDisambiguation from decopatch.utils_modes import SignatureInfo try: from typing import ParamSpec except ImportError: from typing_extensions import ParamSpec P = ParamSpec("P") F = TypeVar("F", bound=Callable[..., Any]) class _Decorator(Protocol[P]): """ This is callable Protocol, to distinguish between cases where created decorator is called as `@decorator` or `@decorator()` """ # decorator is called without parenthesis: @decorator @overload def __call__(self, func: F) -> F: ... # decorator is called with options or parenthesis: @decorator(some_option=...) @overload def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Callable[[F], F]: ... # @function_decorator is called without options or parenthesis @overload def function_decorator( enable_stack_introspection: Callable[P, Any], custom_disambiguator: Callable[[Any], FirstArgDisambiguation] = ..., flat_mode_decorated_name: Optional[str] = ..., ) -> _Decorator[P]: ... # @function_decorator() is called with options or parenthesis. @overload def function_decorator( enable_stack_introspection: bool = ..., custom_disambiguator: Callable[[Any], FirstArgDisambiguation] = ..., flat_mode_decorated_name: Optional[str] = ..., ) -> Callable[[Callable[P, Any]], _Decorator[P]]: ... def class_decorator( enable_stack_introspection: bool = ..., custom_disambiguator: Callable[[Any], FirstArgDisambiguation] = ..., flat_mode_decorated_name: Optional[str] = ..., ): ... def decorator( is_function_decorator: bool = ..., is_class_decorator: bool = ..., enable_stack_introspection: bool = ..., custom_disambiguator: Callable[[Any], FirstArgDisambiguation] = ..., use_signature_trick: bool = ..., flat_mode_decorated_name: str = ..., ): ... def create_decorator( impl_function, is_function_decorator: bool = ..., is_class_decorator: bool = ..., enable_stack_introspection: bool = ..., custom_disambiguator: Callable[[Any], FirstArgDisambiguation] = ..., use_signature_trick: bool = ..., flat_mode_decorated_name: Optional[str] = ..., ): ... def create_no_args_decorator( decorator_function, function_for_metadata: Any | None = ... ): ... def create_kwonly_decorator( sig_info: SignatureInfo, decorator_function, disambiguator, function_for_metadata ): ... def create_general_case_decorator( sig_info: SignatureInfo, impl_function, disambiguator, function_for_metadata ): ... ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/src/decopatch/py.typed������������������������������������������������������0000664�0000000�0000000�00000000000�14207357143�0021262�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/src/decopatch/utils_calls.py������������������������������������������������0000664�0000000�0000000�00000007052�14207357143�0022471�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from decopatch.utils_disambiguation import FirstArgDisambiguation class AmbiguousFirstArgumentTypeError(TypeError): pass class InvalidMandatoryArgError(TypeError): pass def call_in_appropriate_mode(impl_function, dk, # type: DecoratorUsageInfo disambiguation_result # type: FirstArgDisambiguation ): """ :param impl_function: :param dk: :param disambiguation_result: :return: """ if disambiguation_result is FirstArgDisambiguation.is_decorated_target: # (1) NO-parenthesis usage: @foo_decorator if dk.sig_info.is_first_arg_mandatory: # that's not possible raise InvalidMandatoryArgError("function '%s' requires a mandatory argument '%s'. Provided value '%s' does " "not pass its validation criteria" "" % (impl_function.__name__, dk.sig_info.first_arg_name_with_possible_star, dk.first_arg_value)) else: # ok: do it return no_parenthesis_usage(impl_function, dk.first_arg_value) elif disambiguation_result is FirstArgDisambiguation.is_normal_arg: # (2) WITH-parenthesis usage: @foo_decorator(*args, **kwargs). return with_parenthesis_usage(impl_function, *dk.args, **dk.kwargs) elif disambiguation_result is FirstArgDisambiguation.is_ambiguous: # (3) STILL AMBIGUOUS # By default we are very conservative: we do not allow the first argument to be a callable or class if user did # not provide a way to disambiguate it if dk.sig_info.is_first_arg_mandatory: raise AmbiguousFirstArgumentTypeError( "function '%s' requires a mandatory argument '%s'. It cannot be a class nor a callable." " If you think that it should, then ask your decorator provider to protect his decorator (see " "decopath documentation)" % (impl_function.__name__, dk.sig_info.first_arg_name_with_possible_star)) else: raise AmbiguousFirstArgumentTypeError( "Argument '%s' of generated decorator function '%s' is the *first* argument in the signature. " "When the decorator is called (1) with only this argument as non-default value and (2) if this " "argument is a callable or class, then it is not possible to determine if that call was a " "no-parenthesis decorator usage or a with-args decorator usage. If you think that this particular " "usage should be allowed, then ask your decorator provider to protect his decorator (see decopath " "documentation)" % (dk.sig_info.first_arg_name_with_possible_star, impl_function.__name__)) else: raise ValueError("single-argument disambiguation did not return properly: received %s" % disambiguation_result) def no_parenthesis_usage(decorator_function, decorated): """ called with no arg NOR parenthesis: @foo_decorator we have to directly apply the decorator :param decorated: :param decorator_function: :return: """ return decorator_function()(decorated) def with_parenthesis_usage(decorator_function, *args, **kwargs): """ called with no args BUT parenthesis: @foo_decorator(). we have to return a nested function to apply the decorator :param decorator_function: :param args: :param kwargs: :return: """ return decorator_function(*args, **kwargs) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/src/decopatch/utils_disambiguation.py���������������������������������������0000664�0000000�0000000�00000031055�14207357143�0024372�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import sys from enum import Enum from inspect import isclass from linecache import getline from warnings import warn from decopatch.utils_modes import SignatureInfo class FirstArgDisambiguation(Enum): """ This enum is used for the output of user-provided first argument disambiguators. """ is_normal_arg = 0 is_decorated_target = 1 is_ambiguous = 2 _WITH_PARENTHESIS = FirstArgDisambiguation.is_normal_arg """Alias for the case where the arg is a normal arg""" _NO_PARENTHESIS = FirstArgDisambiguation.is_decorated_target """Alias for the case where the arg is a decorated target""" def with_parenthesis(ambiguous_first_arg): """hardcoded disambiguator to say that in case of doubt, it is probably a with-parenthesis call""" return _WITH_PARENTHESIS def no_parenthesis(ambiguous_first_arg): """hardcoded disambiguator to say that in case of doubt, it is probably a no-parenthesis""" return _NO_PARENTHESIS def can_arg_be_a_decorator_target(arg): """ Returns True if the argument received has a type that can be decorated. If this method returns False, we are sure that this is a *with*-parenthesis call because python does not allow you to decorate anything else than a function or a class :param arg: :return: """ return callable(arg) or isclass(arg) class DecoratorUsageInfo(object): """ Represent the knowledge that we have when the decorator is being used. Note: arguments binding is computed in a lazy mode (only if needed) """ __slots__ = 'sig_info', '_first_arg_value', '_bound', 'args', 'kwargs' def __init__(self, sig_info, # type: SignatureInfo args, kwargs): self.sig_info = sig_info self.args = args self.kwargs = kwargs self._first_arg_value = DecoratorUsageInfo # this is our way to say 'uninitialized' self._bound = None @property def first_arg_value(self): if self._first_arg_value is DecoratorUsageInfo: # not yet initialized self._first_arg_value = self.bound.arguments[self.sig_info.first_arg_name] return self._first_arg_value @property def bound(self): if self._bound is None: self._bound = self.sig_info.exposed_signature.bind(*self.args, **self.kwargs) return self._bound def disambiguate_call(dk, # type: DecoratorUsageInfo disambiguator): """ :return: """ # (1) use the number of args to eliminate a few cases if dk.sig_info.use_signature_trick: # we expose a *args, **kwargs signature so we see exactly what the user has provided. nb_pos_received, nb_kw_received = len(dk.args), len(dk.kwargs) if nb_kw_received > 0 or nb_pos_received == 0 or nb_pos_received > 1: return _WITH_PARENTHESIS dk._first_arg_value = dk.args[0] else: # we expose a "true" signature, the only arguments that remain in *args are the ones that CANNOT become kw nb_posonly_received = len(dk.args) if dk.sig_info.contains_varpositional or dk.sig_info.is_first_arg_positional_only: if nb_posonly_received == 0: # with parenthesis: @foo_decorator(**kwargs) return _WITH_PARENTHESIS elif nb_posonly_received >= 2: # with parenthesis: @foo_decorator(a, b, **kwargs) return _WITH_PARENTHESIS else: # AMBIGUOUS: # no parenthesis: @foo_decorator -OR- with 1 positional argument: @foo_decorator(a, **kwargs). # reminder: we can not count the kwargs because they always contain all the arguments dk._first_arg_value = dk.args[0] else: # first arg can be keyword. So it will be in kwargs (even if it was provided as positional). if nb_posonly_received > 0: raise Exception("Internal error - this should not happen, please file an issue on the github page") # (2) Now work on the values themselves if not can_arg_be_a_decorator_target(dk.first_arg_value): # the first argument can NOT be decorated: we are sure that this was a WITH-parenthesis call return _WITH_PARENTHESIS elif not dk.sig_info.use_signature_trick: # we were not able to use the length of kwargs, but at least we can compare them to defaults. if dk.first_arg_value is dk.sig_info.first_arg_def.default: # the first argument has NOT been set, we are sure that's WITH-parenthesis call return _WITH_PARENTHESIS else: # check if there is another argument that is different from its default value # skip first entry params = iter(dk.sig_info.exposed_signature.parameters.items()) next(params) for p_name, p_def in params: try: if dk.bound.arguments[p_name] is not p_def.default: return _WITH_PARENTHESIS except KeyError: pass # this can happen when the argument is **kwargs and not filled: it does not even appear. # (3) still-ambiguous case, the first parameter is the single non-default one and is a callable or class # at this point a no-parenthesis call is still possible. # call disambiguator (it combines our rules with the ones optionally provided by the user) return disambiguator(dk.first_arg_value) def create_single_arg_callable_or_class_disambiguator(impl_function, is_function_decorator, is_class_decorator, custom_disambiguator, enable_stack_introspection, signature_knowledge, # type: SignatureInfo ): """ Returns the function that should be used as disambiguator in "last resort" (when the first argument is the single non-default argument and it is a callable/class). :param first_arg_received: :return: """ def _disambiguate_call(first_arg_received): """ The first argument received is the single non-default argument and it is a callable/class. We should try to disambiguate now. :param first_arg_received: :return: """ # introspection-based if enable_stack_introspection: depth = 4 if signature_knowledge.use_signature_trick else 5 try: res = disambiguate_using_introspection(depth, first_arg_received) if res is not None: return res except IPythonException as e: warn("Decorator disambiguation using stack introspection is not available in Jupyter/iPython. " "Please use the decorator in a non-ambiguous way. For example use explicit parenthesis @%s() " "for no-arg usage, or use 2 non-default arguments, or use explicit keywords. Ambiguous " "argument received: %s." % (impl_function.__name__, first_arg_received)) except Exception: # silently escape all exceptions by default - safer for users pass # we want to eliminate as much as possible the args that cannot be first args if callable(first_arg_received) and not isclass(first_arg_received) and not is_function_decorator: # that function cannot be a decorator target so it has to be the first argument return FirstArgDisambiguation.is_normal_arg elif isclass(first_arg_received) and not is_class_decorator: # that class cannot be a decorator target so it has to be the first argument return FirstArgDisambiguation.is_normal_arg elif custom_disambiguator is not None: # an explicit disambiguator is provided, use it return custom_disambiguator(first_arg_received) else: # Always say "decorated target" because # - if 1+ mandatory arg, we can safely do this, it will raise a `TypeError` automatically # - if 0 mandatory args, the most probable scenario for receiving a single callable or class argument is # still the no-arg. We do not want to penalize users. return FirstArgDisambiguation.is_decorated_target return _disambiguate_call class IPythonException(Exception): """Exception raised by `disambiguate_using_introspection` when the file where the decorator was used seems to be an ipython one""" pass SUPPORTS_INTROSPECTION = sys.version_info < (3, 8) def disambiguate_using_introspection(depth, first_arg): """ Tries to disambiguate the call situation betwen with-parenthesis and without-parenthesis using call stack introspection. Uses inpect.stack() to get the source code where the decorator is being used. If the line starts with a '@' and does not contain any '(', this is a no-parenthesis call. Otherwise it is a with-parenthesis call. TODO it could be worth investigating how to improve this logic.. but remember that the decorator can also be renamed so we can not check a full string expression :param depth: :return: """ if not SUPPORTS_INTROSPECTION: # This does not seem to work reliably. raise NotImplementedError("The beta stack introspection feature does not support python 3.8+. Please set" " `enable_stack_introspection=False`.") # Unfortunately inspect.stack and inspect.currentframe are extremely slow # see https://gist.github.com/JettJones/c236494013f22723c1822126df944b12 # -- # curframe = currentframe() # calframe = getouterframes(curframe, 4) # --or # calframe = stack(context=1) # filename = calframe[depth][1] # ---- # this is fast :) calframe = sys._getframe(depth) try: # if target is a function that should work is_decorator_call_ = first_arg.__code__.co_firstlineno == calframe.f_lineno except AttributeError: # if target is a class rely on source code using linecache is_decorator_call_ = is_decorator_call(calframe) if is_decorator_call_: return FirstArgDisambiguation.is_decorated_target else: return FirstArgDisambiguation.is_normal_arg # try: # from opcode import opmap # # def is_decorator_call(frame, first_arg): # """ # This implementation relies on bytecode inspection. # It seems to change too much across versions to be reliable. # # :param frame: # :return: # """ # try: # # if target is a function that should work # return first_arg.__code__.co_firstlineno == frame.f_lineno # except AttributeError: # pass # # # if the last bytecode operation before this frame is a MAKE_FUNCTION or a LOAD_BUILD_CLASS, # # that means that we are a decorator without arguments >> NO, that's wrong !!!! # # --rather # # if the last LOAD_CONST done is for a <code> object that lies in the same line, then that's without arguments. # code = frame.f_code.co_code # opcode = 0 # instruction_idx = frame.f_lasti # # instructions are every 3 positions # last_load_const_idx = instruction_idx - (5 * 3) # opcode = code[last_load_const_idx] # # import dis # dis.disassemble(frame.f_code, lasti=instruction_idx) # lasti=frame.f_lasti # # if opcode != opmap['LOAD_CONST']: # return False # else: # # is this loading a code that lies after the frame.f_lineno ? # arg = code[last_load_const_idx + 1] + code[last_load_const_idx + 2] * 256 # # argval, argrepr = _get_const_info(arg, constants) # target = frame.f_code.co_consts[arg] # # return target.co_firstlineno == frame.f_lineno # # except ImportError: def is_decorator_call(frame): """ This implementation relies on source code inspection thanks to linecache. :param frame: :return: """ # using inspect.getsource : heavy... # using traceback.format_stack: seems to fallback to linecache anyway. # using linecache https://docs.python.org/3/library/linecache.html cal_line_str = getline(frame.f_code.co_filename, frame.f_lineno).strip() if cal_line_str.startswith('class'): cal_line_str = getline(frame.f_code.co_filename, frame.f_lineno - 1).strip() return cal_line_str.startswith('@') and '(' not in cal_line_str �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/src/decopatch/utils_modes.py������������������������������������������������0000664�0000000�0000000�00000035560�14207357143�0022507�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from makefun import remove_signature_parameters, with_signature, wraps try: # python 3.3+ from inspect import signature, Parameter funcsigs_used = False except ImportError: from funcsigs import signature, Parameter funcsigs_used = True class _Symbol: """ Symbols used in your (double) flat-mode signatures to declare where the various objects should be injected These symbols have a nice representation. """ __slots__ = ('repr_', ) def __init__(self, repr_): self.repr_ = repr_ def __repr__(self): return self.repr_ DECORATED = _Symbol('DECORATED') # A symbol used in flat-mode signatures to declare where the decorated function should be injected WRAPPED = _Symbol('WRAPPED') # A symbol used in double flat-mode signatures to declare where the wrapped function should be injected F_ARGS = _Symbol('F_ARGS') # A symbol used in your double flat-mode signatures to declare where the wrapper args should be injected F_KWARGS = _Symbol('F_KWARGS') # A symbol used in your double flat-mode signatures to declare where the wrapper kwargs should be injected def make_decorator_spec(impl_function, flat_mode_decorated_name=None # type: str ): """ Analyzes the implementation function If `flat_mode_decorated_name` is set, this is a shortcut for flat mode. In that case the implementation function is not analyzed. :param impl_function: :param flat_mode_decorated_name: :return: sig_info, function_for_metadata, nested_impl_function """ # extract the implementation's signature implementors_signature = signature(impl_function) # determine the mode (nested, flat, double-flat) and check signature mode, injected_name, contains_varpositional, injected_pos, \ injected_arg, f_args_name, f_kwargs_name = extract_mode_info(implementors_signature, flat_mode_decorated_name) # create the signature of the decorator function to create, according to mode if mode is None: # *nested: keep the signature 'as is' exposed_signature = implementors_signature function_for_metadata = impl_function nested_impl_function = impl_function elif mode is DECORATED: # flat mode # use the same signature, but remove the injected arg. exposed_signature = remove_signature_parameters(implementors_signature, injected_name) # use the original function for the docstring/module metadata function_for_metadata = impl_function # generate the corresponding nested decorator nested_impl_function = make_nested_impl_for_flat_mode(exposed_signature, impl_function, injected_name, injected_pos) elif mode is WRAPPED: # *double-flat: the same signature, but we remove the injected args. args_to_remove = (injected_name,) + ((f_args_name,) if f_args_name is not None else ()) \ + ((f_kwargs_name,) if f_kwargs_name is not None else ()) exposed_signature = remove_signature_parameters(implementors_signature, *args_to_remove) # use the original function for the docstring/module metadata function_for_metadata = impl_function # generate the corresponding nested decorator nested_impl_function = make_nested_impl_for_doubleflat_mode(exposed_signature, impl_function, injected_name, f_args_name, f_kwargs_name, injected_pos) else: raise ValueError("Unknown mode: %s" % mode) # create an object to easily access the exposed signature information afterwards sig_info = SignatureInfo(exposed_signature, contains_varpositional, injected_pos) return sig_info, function_for_metadata, nested_impl_function def make_nested_impl_for_flat_mode(decorator_signature, user_provided_applier, injected_name, injected_pos): """ Creates the nested-mode decorator to be used when the implementation is provided in flat mode. Note: we set the signature correctly so that this behaves exactly like a nested implementation in terms of exceptions raised when the arguments are incorrect. Since the external method is called only once per decorator usage and does not impact the decorated object we can afford. :param decorator_signature: :param user_provided_applier: :param injected_name: :param argnames_before_varpos_arg: :return: """ @with_signature(decorator_signature) def _decorator(*args, **kwargs): """ The decorator. Its signature will be overriden by `generated_signature` """ def _apply_decorator(decorated): """ This is called when the decorator is applied to an object `decorated` """ # inject `decorated` under the correct name # fix in case of var-positional arguments if injected_pos >= 0: new_args = args[:injected_pos] + (decorated, ) + args[injected_pos:] else: new_args = args kwargs[injected_name] = decorated return user_provided_applier(*new_args, **kwargs) return _apply_decorator return _decorator def make_nested_impl_for_doubleflat_mode(decorator_signature, user_provided_wrapper, injected_name, f_args_name, f_kwargs_name, injected_pos): """ Creates the nested-mode decorator to be used when the implementation is provided in double-flat mode. Note: we set the signature correctly so that this behaves exactly like a nested implementation in terms of exceptions raised when the arguments are incorrect. Since the external method is called only once per decorator usage and does not impact the decorated object / created wrappe, we can afford. :param decorator_signature: :param user_provided_wrapper: :param injected_name: :param f_args_name: :param f_kwargs_name: :return: """ @with_signature(decorator_signature) def _decorator(*args, **kwargs): """ The decorator. Its signature will be overriden by `generated_signature` """ def _apply_decorator(decorated): """ This is called when the decorator is applied to an object `decorated` """ # inject `decorated` under the correct name # fix in case of var-positional arguments if injected_pos >= 0: new_args = args[:injected_pos] + (decorated,) + args[injected_pos:] else: new_args = args kwargs[injected_name] = decorated # create a signature-preserving wrapper using `makefun.wraps` @wraps(decorated) def wrapper(*f_args, **f_kwargs): # if the user wishes us to inject the actual args and kwargs, let's inject them # note: for these it is always keyword-based. if f_args_name is not None: kwargs[f_args_name] = f_args if f_kwargs_name is not None: kwargs[f_kwargs_name] = f_kwargs # finally call the user-provided implementation return user_provided_wrapper(*new_args, **kwargs) return wrapper return _apply_decorator return _decorator class InvalidSignatureError(Exception): """ Exception raised when a decorator signature is invalid with respect to the selected mode. Typically when you use flat-mode or wrapped-mode symbols but your signature does not allow them to be safely injected as keyword because they are followed by a var-positional argument. """ pass def extract_mode_info(impl_sig, # type: Signature flat_mode_decorated_name=None # type: str ): """ Returns the (name, Parameter) for the parameter with default value DECORATED :param impl_sig: the implementing function's signature :param flat_mode_decorated_name: an optional name of decorated argument. If provided a "flat mode" is automatically set :return: """ mode = None injected = None injected_pos = None position_of_varpos = -1 f_args = None f_kwargs = None if flat_mode_decorated_name is not None: # validate that the 'decorated' parameter is a string representing a real parameter of the function if not isinstance(flat_mode_decorated_name, str): raise InvalidSignatureError("'flat_mode_decorated_name' argument should be a string with the argument name " "where the wrapped object should be injected") mode = DECORATED # analyze signature to detect injected arg and potentially varpositional for i, (k, p) in enumerate(impl_sig.parameters.items()): if k == flat_mode_decorated_name: # this is the injected parameter injected = p injected_pos = i elif p.kind is Parameter.VAR_POSITIONAL: position_of_varpos = i if injected is None: return ValueError("Function '%s' does not have an argument named '%s'" % (impl_sig.__name__, flat_mode_decorated_name)) if injected.kind in {Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD}: raise InvalidSignatureError("`flat_mode_decorated_name` cannot correspond to a Var-pos nor Var-kw") else: # analyze signature to detect for i, (p_name, p) in enumerate(impl_sig.parameters.items()): if p.kind is Parameter.VAR_POSITIONAL: position_of_varpos = i if f_args is not None or f_kwargs is not None: raise InvalidSignatureError("f_args and f_kwargs can only be used *after* var-positional arguments") elif p.default is DECORATED: if mode is not None: raise InvalidSignatureError("only one of `DECORATED` or `WRAPPED` can be used in your signature") else: mode = DECORATED injected = p injected_pos = i elif p.default is WRAPPED: if mode is not None: raise InvalidSignatureError("only one of `DECORATED` or `WRAPPED` can be used in your signature") else: mode = WRAPPED injected = p injected_pos = i elif p.default is F_ARGS: f_args = p elif p.default is F_KWARGS: f_kwargs = p if mode in {None, DECORATED} and (f_args is not None or f_kwargs is not None): raise InvalidSignatureError("`F_ARGS` or `F_KWARGS` should only be used if you use `WRAPPED`") # argnames_before_varpos_arg = None # if position_of_varpos > 0: # # if there is a var-positional we will have to inject arguments before it manually # argnames_before_varpos_arg = tuple(k for k in list(impl_sig.parameters.keys())[0:position_of_varpos]) # # if argnames_before_varpos_arg is None: # argnames_before_varpos_arg = tuple() contains_varpositional = position_of_varpos >= 0 if not contains_varpositional or (injected_pos is not None and position_of_varpos < injected_pos): # do not inject as positional but as keyword argument injected_pos = -1 return mode, (injected.name if injected is not None else None), contains_varpositional, injected_pos, \ injected, (f_args.name if f_args is not None else None), (f_kwargs.name if f_kwargs is not None else None) # ----------- class SignatureInfo(object): """ Represents the knowledge we have on the decorator signature. Provides handy properties to separate the code requirements from the implementation (and possibly cache). """ __slots__ = '_exposed_signature', 'first_arg_def', '_use_signature_trick', 'contains_varpositional', \ 'injected_pos' def __init__(self, decorator_signature, contains_varpositional, injected_pos): self._exposed_signature = decorator_signature _, self.first_arg_def = get_first_parameter(decorator_signature) self._use_signature_trick = False self.contains_varpositional = contains_varpositional self.injected_pos = injected_pos # -- @property def use_signature_trick(self): return self._use_signature_trick @use_signature_trick.setter def use_signature_trick(self, use_signature_trick): # note: as of today python 2.7 backport does not handle it properly, but hopefully it will :) # see https://github.com/testing-cabal/funcsigs/issues/33. self._use_signature_trick = use_signature_trick and not funcsigs_used # -- @property def exposed_signature(self): return self._exposed_signature @exposed_signature.setter def exposed_signature(self, new_sig): """ If the signature is changed then we should be careful.. :param new_sig: :return: """ # this currently only happen in a single specific case, control that to avoid future mistakes if self.first_arg_kind is not Parameter.VAR_KEYWORD or len(self._exposed_signature.parameters) != 1: raise NotImplementedError("This case should not happen") self._exposed_signature = new_sig self.contains_varpositional = any(p.kind is Parameter.VAR_POSITIONAL for p in new_sig.parameters.values()) _, self.first_arg_def = get_first_parameter(new_sig) # -- @property def first_arg_name(self): return self.first_arg_def.name # if self.first_arg_def is not None else None @property def first_arg_name_with_possible_star(self): return ('*' if self.is_first_arg_varpositional else '') + self.first_arg_name @property def first_arg_kind(self): return self.first_arg_def.kind # if self.first_arg_def is not None else None @property def is_first_arg_keyword_only(self): return self.first_arg_kind in {Parameter.KEYWORD_ONLY, Parameter.VAR_KEYWORD} @property def is_first_arg_varpositional(self): return self.first_arg_kind is Parameter.VAR_POSITIONAL @property def is_first_arg_positional_only(self): return self.first_arg_kind is Parameter.POSITIONAL_ONLY @property def is_first_arg_mandatory(self): return self.first_arg_def.default is Parameter.empty and self.first_arg_kind not in {Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD} def get_first_parameter(ds # type: Signature ): """ Returns the (name, Parameter) for the first parameter in the signature :param ds: :return: """ try: return next(iter(ds.parameters.items())) except StopIteration: return None, None ������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/tests/����������������������������������������������������������������������0000775�0000000�0000000�00000000000�14207357143�0016216�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/tests/__init__.py�����������������������������������������������������������0000664�0000000�0000000�00000000000�14207357143�0020315�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/tests/_test_doc_py3.py������������������������������������������������������0000664�0000000�0000000�00000007020�14207357143�0021325�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from decopatch import function_decorator, DECORATED, decorator, F_ARGS, F_KWARGS, WRAPPED def create_test_doc_impl_first_tag_mandatory_protected_with_star(uses_introspection): @function_decorator(enable_stack_introspection=uses_introspection) def add_tag(*, tag, f=DECORATED): setattr(f, 'my_tag', tag) return f return add_tag def create_test_doc_impl_first_tag_optional_nonprotected_star(): """Tests that an error is raised when nonprotected code is created """ @function_decorator def add_tag(*, tag='tag!'): """ This decorator adds the 'my_tag' tag on the decorated function, with the value provided as argument :param tag: the tag value to set :return: """ def _apply_on(f): setattr(f, 'my_tag', tag) return f return _apply_on return add_tag def create_test_doc_impl_first_tag_optional_protected(uses_introspection): """ The second implementation-first example in the doc """ # protect it explicitly if introspection is disabled @function_decorator(enable_stack_introspection=uses_introspection) def add_tag(*, tag='tag!', f=DECORATED): """ This decorator adds the 'my_tag' tag on the decorated function, with the value provided as argument :param tag: the tag value to set :param f: represents the decorated item. Automatically injected. :return: """ setattr(f, 'my_tag', tag) return f return add_tag def create_test_wrapped_bad_signature(test_nb, *ref_tags): """ :param test_nb: :return: """ if test_nb == 0: @function_decorator def foo(func=WRAPPED, *tags, f_args=F_ARGS, f_kwargs=F_KWARGS): assert tags == ref_tags elif test_nb == 1: @function_decorator def foo(f_args=F_ARGS, *tags, func=WRAPPED, f_kwargs=F_KWARGS): assert tags == ref_tags elif test_nb == 2: @function_decorator def foo(f_kwargs=F_KWARGS, *tags, func=WRAPPED, f_args=F_ARGS): assert tags == ref_tags elif test_nb == 3: @function_decorator def foo(f_kwargs=F_KWARGS, func=WRAPPED, *tags, f_args=F_ARGS): assert tags == ref_tags return foo # --------- from test_doc_disambiguation.py def create_test_doc_disambiguation_kwonly_mandatory(flat_mode): """ """ if not flat_mode: @decorator def replace_with(*, replacement): """ Decorator to replace anything with the <replacement> object. """ def _apply_decorator(f): return replacement return _apply_decorator else: @decorator def replace_with(*, replacement, f=DECORATED): """ Decorator to replace anything with the <replacement> object. """ return replacement return replace_with def create_test_doc_disambiguation_kwonly_optional(flat_mode): """ """ if not flat_mode: @decorator def replace_with(*, replacement='hello'): """ Decorator to replace anything with the <replacement> object. """ def _apply_decorator(f): return replacement return _apply_decorator else: @decorator def replace_with(*, replacement='hello', f=DECORATED): """ Decorator to replace anything with the <replacement> object. """ return replacement return replace_with ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/tests/_test_issues_py3.py���������������������������������������������������0000664�0000000�0000000�00000000773�14207357143�0022103�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Authors: Sylvain Marie <sylvain.marie@se.com> # # Copyright (c) Schneider Electric Industries, 2019. All right reserved. def create_test_varpositional(): from decopatch import class_decorator, DECORATED @class_decorator def replace_with(*args, cls=DECORATED): # lets make sure that DECORATED has been injected assert cls is not DECORATED # and replace the class with the tuple of varpos arguments, just for easy check return args return replace_with �����python-decopatch-1.4.10/tests/test_doc.py�����������������������������������������������������������0000664�0000000�0000000�00000017607�14207357143�0020407�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import print_function import pytest from makefun import wraps from decopatch import function_decorator, DECORATED, WRAPPED, F_ARGS, F_KWARGS, decorator try: # python 3.3+ from inspect import signature except ImportError: from funcsigs import signature @pytest.mark.parametrize('flat_mode', [False, True], ids="flat_mode={}".format) def test_doc_add_tag_function(flat_mode): """ Tests that the @add_tag example from doc (functions only) works """ if not flat_mode: @function_decorator def add_tag(tag='hi!'): """ Example decorator to add a 'tag' attribute to a function. :param tag: the 'tag' value to set on the decorated function (default 'hi!). """ def apply_decorator(f): """ This is the method that will be called when your decorator is used on a function `f`. It should return the replacement for this function (it can be the same object, or another one, not even a function!) """ setattr(f, 'tag', tag) return f return apply_decorator else: @function_decorator def add_tag(tag='hi!', f=DECORATED): """ Example decorator to add a 'tag' attribute to a function. :param tag: the 'tag' value to set on the decorated function (default 'hi!). """ setattr(f, 'tag', tag) return f @add_tag def foo(): pass assert foo.tag == 'hi!' @add_tag() def foo(): pass assert foo.tag == 'hi!' @add_tag('hello') def foo(): pass assert foo.tag == 'hello' # manual mode add_tag()(foo) assert foo.tag == 'hi!' # signature and help assert "%s%s" % (add_tag.__name__, signature(add_tag)) == "add_tag(tag='hi!')" print(help(add_tag)) assert add_tag.__doc__ == """ Example decorator to add a 'tag' attribute to a function. :param tag: the 'tag' value to set on the decorated function (default 'hi!). """ assert add_tag.__module__ == test_doc_add_tag_function.__module__ @pytest.mark.parametrize('mode', ['nested', 'flat', 'double-flat']) def test_doc_say_hello(capsys, mode): """ Tests that the @say_hello example from doc works """ with capsys.disabled(): if mode == 'nested': @function_decorator def say_hello(person="world", f=DECORATED): """ This decorator wraps the decorated function so that a nice hello message is printed before each call. :param person: the person name in the print message. Default = "world" """ # create a wrapper of f that will do the print before call # we rely on `makefun.wraps` to preserve signature @wraps(f) def new_f(*args, **kwargs): print("hello, %s !" % person) # say hello return f(*args, **kwargs) # call f # return the new function return new_f elif mode == 'flat': @function_decorator def say_hello(person="world", f=DECORATED): """ This decorator wraps the decorated function so that a nice hello message is printed before each call. :param person: the person name in the print message. Default = "world" """ # create a wrapper of f that will do the print before call # we rely on `makefun.wraps` to preserve signature @wraps(f) def new_f(*args, **kwargs): print("hello, %s !" % person) # say hello return f(*args, **kwargs) # call f # return the new function return new_f elif mode == 'double-flat': @function_decorator def say_hello(person="world", f=WRAPPED, f_args=F_ARGS, f_kwargs=F_KWARGS): """ This decorator wraps the decorated function so that a nice hello message is printed before each call. :param person: the person name in the print message. Default = "world" """ print("hello, %s !" % person) # say hello return f(*f_args, **f_kwargs) # call f else: raise ValueError("unsupported mode : %s" % mode) # for debug.. with capsys.disabled(): @say_hello # no parenthesis def foo(): print("<executing foo>") # for debug.. with capsys.disabled(): foo() foo() @say_hello() # empty parenthesis def bar(): print("<executing bar>") bar() @say_hello("you") # arg def custom(): print("<executing custom>") custom() # manual decoration def custom2(): print("<executing custom2>") custom2 = say_hello()(custom2) custom2() help(say_hello) assert str(signature(say_hello)) == "(person='world')" print("Signature: %s" % signature(say_hello)) @say_hello # no parenthesis def add_ints(a, b): return a + b assert add_ints(1, 3) == 4 captured = capsys.readouterr() with capsys.disabled(): print(captured.out) assert captured.out == """hello, world ! <executing foo> hello, world ! <executing bar> hello, you ! <executing custom> hello, world ! <executing custom2> Help on function say_hello in module tests.test_doc: say_hello(person='world') This decorator wraps the decorated function so that a nice hello message is printed before each call. :param person: the person name in the print message. Default = "world" Signature: (person='world') hello, world ! """ assert captured.err == "" @pytest.mark.parametrize('flat_mode', [False, True], ids="flat_mode={}".format) def test_doc_add_tag_class_and_function(flat_mode): """ Tests that the @add_tag example from doc (function + class) works """ if not flat_mode: @decorator def add_tag(tag='hi!'): """ Example decorator to add a 'tag' attribute to a function or class. It can be used with and without parenthesis, with and without arguments. :param tag: the 'tag' value to set on the decorated item (default 'hi!). """ def _apply_decorator(o): """ This is the method that will be called when your decorator is used on an object `i`. It should return the replacement for this object (it can be the same object, or another one, not even the same type!) """ setattr(o, 'tag', tag) return o return _apply_decorator else: @decorator def add_tag(tag='hi!', o=DECORATED): """ Example decorator to add a 'tag' attribute to a function or class. It can be used with and without parenthesis, with and without arguments. :param tag: the 'tag' value to set on the decorated item (default 'hi!). """ setattr(o, 'tag', tag) return o @add_tag def foo(): pass assert foo.tag == 'hi!' @add_tag() def foo(): pass assert foo.tag == 'hi!' @add_tag('hello') def foo(): pass assert foo.tag == 'hello' # manual mode add_tag()(foo) assert foo.tag == 'hi!' # classes @add_tag class Foo(): pass assert Foo.tag == 'hi!' @add_tag() class Foo(): pass assert Foo.tag == 'hi!' @add_tag('hello') class Foo(): pass assert Foo.tag == 'hello' # manual mode add_tag()(Foo) assert Foo.tag == 'hi!' print(help(add_tag)) assert str(signature(add_tag)) == "(tag='hi!')" �������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/tests/test_doc_advanced.py��������������������������������������������������0000664�0000000�0000000�00000033730�14207357143�0022227�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import print_function import sys import pytest from makefun import wraps from decopatch import function_decorator, DECORATED, InvalidMandatoryArgError, class_decorator, InvalidSignatureError, \ WRAPPED from decopatch.utils_disambiguation import SUPPORTS_INTROSPECTION try: # python 3.3+ from inspect import signature funcsigs_used = False except ImportError: from funcsigs import signature funcsigs_used = True @pytest.mark.parametrize('nested_mode', [True, False], ids="nested_mode={}".format) @pytest.mark.parametrize('uses_introspection', [ pytest.param(True, marks=pytest.mark.skipif(not SUPPORTS_INTROSPECTION, reason="not available on python 3.8+")), False ], ids="uses_introspection={}".format) def test_doc_impl_first_tag_mandatory(uses_introspection, nested_mode): """ The first implementation-first example in the doc """ if nested_mode: @function_decorator(enable_stack_introspection=uses_introspection) def add_tag(tag): """ This decorator adds the 'my_tag' tag on the decorated function, with the value provided as argument :param tag: the tag value to set :param f: represents the decorated item. Automatically injected. :return: """ def replace_f(f): setattr(f, 'my_tag', tag) return f return replace_f else: @function_decorator(enable_stack_introspection=uses_introspection) def add_tag(tag, f=DECORATED): """ This decorator adds the 'my_tag' tag on the decorated function, with the value provided as argument :param tag: the tag value to set :param f: represents the decorated item. Automatically injected. :return: """ setattr(f, 'my_tag', tag) return f @add_tag('hello') def foo(): return assert foo.my_tag == 'hello' err_type = TypeError if uses_introspection else InvalidMandatoryArgError with pytest.raises(err_type): # add_tag() missing 1 required positional argument: 'tag' @add_tag def foo(): if True: return if not uses_introspection: with pytest.raises(InvalidMandatoryArgError): # first argument is a callable > problem @add_tag(print) def foo(): return else: # no problem @add_tag(print) def foo(): return assert foo.my_tag == print def test_doc_impl_first_tag_optional(): """ The second implementation-first example in the doc """ @function_decorator def add_tag(tag='tag!', f=DECORATED): """ This decorator adds the 'my_tag' tag on the decorated function, with the value provided as argument :param tag: the tag value to set :param f: represents the decorated item. Automatically injected. :return: """ setattr(f, 'my_tag', tag) return f @add_tag('hello') # normal arg def foo(): return assert foo.my_tag == 'hello' @add_tag(tag='hello') # normal kwarg def foo(): return assert foo.my_tag == 'hello' @add_tag # no parenthesis def foo(): return assert foo.my_tag == 'tag!' @add_tag() # empty parenthesis def foo(): return assert foo.my_tag == 'tag!' # callable as first arg should be rejected but cant # @add_tag(print) # def foo(): # return # # assert foo.my_tag == print with pytest.raises(AttributeError): add_tag(print) uses_trick = not funcsigs_used if uses_trick: @add_tag(tag=print) # callable as first kwarg works thanks to the trick def foo(): return assert foo.my_tag == print else: with pytest.raises(AttributeError): add_tag(tag=print) def test_doc_impl_first_say_hello(capsys): """The second implementation-first example in the doc""" @function_decorator def say_hello(person='world', f=DECORATED): """ This decorator modifies the decorated function so that a nice hello message is printed before the call. :param person: the person name in the print message. Default = "world" :param f: represents the decorated item. Automatically injected. :return: a modified version of `f` that will print a hello message before executing """ # create a wrapper of f that will do the print before call # we rely on `makefun.wraps` to preserve signature @wraps(f) def new_f(*args, **kwargs): # nonlocal person person = new_f.person print("hello, %s !" % person) # say hello return f(*args, **kwargs) # call f # we use the trick at https://stackoverflow.com/a/16032631/7262247 # to access the nonlocal 'person' variable in python 2 and 3 # for python 3 only you can use 'nonlocal' https://www.python.org/dev/peps/pep-3104/ new_f.person = person # return the wrapper return new_f @say_hello def foo(a, b): return a + b @say_hello() def bar(a, b): return a + b @say_hello("you") def custom(a, b): return a + b assert foo(1, 3) == 4 assert bar(1, 3) == 4 assert custom(1, 3) == 4 help(say_hello) print("Signature: %s" % signature(say_hello)) captured = capsys.readouterr() with capsys.disabled(): print(captured.out) assert captured.out == """hello, world ! hello, world ! hello, you ! Help on function say_hello in module tests.test_doc_advanced: say_hello(person='world') This decorator modifies the decorated function so that a nice hello message is printed before the call. :param person: the person name in the print message. Default = "world" :param f: represents the decorated item. Automatically injected. :return: a modified version of `f` that will print a hello message before executing Signature: (person='world') """ assert captured.err == "" @pytest.mark.parametrize('uses_introspection', [ pytest.param(True, marks=pytest.mark.skipif(not SUPPORTS_INTROSPECTION, reason="not available on python 3.8+")), False ], ids="uses_introspection={}".format) def test_doc_impl_first_class_tag_mandatory(uses_introspection): """ The first implementation-first example in the doc """ @class_decorator(enable_stack_introspection=uses_introspection) def add_tag(tag, c=DECORATED): """ This decorator adds the 'my_tag' tag on the decorated function, with the value provided as argument :param tag: the tag value to set :param c: represents the decorated item. Automatically injected. :return: """ setattr(c, 'my_tag', tag) return c @add_tag('hello') class Foo(object): pass assert Foo.my_tag == 'hello' err_type = InvalidMandatoryArgError # if not uses_introspection else TypeError with pytest.raises(err_type): # add_tag() missing 1 required positional argument: 'tag' @add_tag class Foo(object): def __init__(self): if True: pass def blah(self): pass if not uses_introspection: with pytest.raises(InvalidMandatoryArgError): # first argument is a class > problem @add_tag(object) class Foo(object): pass else: # no problem @add_tag(object) class Foo(object): pass assert Foo.my_tag == object def test_doc_nested_mode_tag_mandatory(): @function_decorator def add_tag(tag): """ This decorator adds the 'my_tag' tag on the decorated function, with the value provided as argument :param tag: the tag value to set :param f: represents the decorated item. Automatically injected. :return: """ def replace_f(f): setattr(f, 'my_tag', tag) return f return replace_f # ----------- more complex / other tests derived from the above for the advanced section @pytest.mark.parametrize('with_star', [True], ids="kwonly={}".format) @pytest.mark.parametrize('uses_introspection', [True, False], ids="uses_introspection={}".format) def test_doc_impl_first_tag_mandatory_protected(with_star, uses_introspection): if with_star: if sys.version_info < (3, 0): pytest.skip("test skipped in python 2.x because kw only is not syntactically correct") else: from ._test_doc_py3 import create_test_doc_impl_first_tag_mandatory_protected_with_star add_tag = create_test_doc_impl_first_tag_mandatory_protected_with_star(uses_introspection) else: raise NotImplementedError() @add_tag(tag='hello') def foo(): return assert foo.my_tag == 'hello' with pytest.raises(TypeError): # add_tag() missing 1 required positional argument: 'tag' @add_tag def foo(): return @add_tag(tag=print) def foo(): return assert foo.my_tag == print @pytest.mark.parametrize('with_star', [False, True], ids="kwonly={}".format) def test_doc_impl_first_tag_optional_nonprotected(with_star): """Tests that an error is raised when nonprotected code is created """ # with pytest.raises(AmbiguousDecoratorDefinitionError): if with_star: if sys.version_info < (3, 0): pytest.skip("test skipped in python 2.x because kw only is not syntactically correct") else: from ._test_doc_py3 import create_test_doc_impl_first_tag_optional_nonprotected_star add_tag = create_test_doc_impl_first_tag_optional_nonprotected_star() else: @function_decorator def add_tag(tag='tag!', f=DECORATED): """ This decorator adds the 'my_tag' tag on the decorated function, with the value provided as argument :param tag: the tag value to set :param f: represents the decorated item. Automatically injected. :return: """ setattr(f, 'my_tag', tag) return f @add_tag(tag='hello') def foo(): return assert foo.my_tag == 'hello' @add_tag def foo(): return assert foo.my_tag == 'tag!' @pytest.mark.parametrize('with_star', [False, True], ids="kwonly={}".format) @pytest.mark.parametrize('uses_introspection', [ pytest.param(True, marks=pytest.mark.skipif(not SUPPORTS_INTROSPECTION, reason="not available on python 3.8+")), False ], ids="introspection={}".format) def test_doc_impl_first_tag_optional_protected(with_star, uses_introspection): """ The second implementation-first example in the doc """ if with_star: if sys.version_info < (3, 0): pytest.skip("test skipped in python 2.x because kw only is not syntactically correct") else: from ._test_doc_py3 import create_test_doc_impl_first_tag_optional_protected add_tag = create_test_doc_impl_first_tag_optional_protected(uses_introspection) else: # protect it explicitly if introspection is disabled @function_decorator(enable_stack_introspection=uses_introspection) def add_tag(tag='tag!', f=DECORATED): """ This decorator adds the 'my_tag' tag on the decorated function, with the value provided as argument :param tag: the tag value to set :param f: represents the decorated item. Automatically injected. :return: """ setattr(f, 'my_tag', tag) return f @add_tag(tag='hello') def foo(): return assert foo.my_tag == 'hello' @add_tag def foo(): return assert foo.my_tag == 'tag!' uses_signature_trick = not funcsigs_used if with_star or uses_introspection or uses_signature_trick: # when we add the star, disambiguation always works (even without introspection @add_tag(tag=print) def foo(): return assert foo.my_tag == print else: # when we do not add the star and no introspection is used and we do not use the trick, there is a problem with pytest.raises(AttributeError): @add_tag(tag=print) def foo(): return if not uses_introspection: with pytest.raises(AttributeError): @add_tag(print) def foo(): return elif with_star: with pytest.raises(TypeError): @add_tag(print) def foo(): return else: @add_tag(print) def foo(): return assert foo.my_tag == print def test_varpos_and_decorated_before_in_flat_mode(): """ Tests that an error is correctly raised if the decorator implementation does not make it possible for the injected args to be injected in flat and double-flat modes. """ @function_decorator def foo(func=DECORATED, *tags): assert tags == ('hello', ) @foo('hello') def f(): pass @function_decorator def foo(func=WRAPPED, *tags): assert tags == ('hello', ) @foo('hello') def f(): pass if sys.version_info >= (3, 0): from ._test_doc_py3 import create_test_wrapped_bad_signature foo = create_test_wrapped_bad_signature(0, 'hello') @foo('hello') def f(): pass for i in range(1, 4): with pytest.raises(InvalidSignatureError): create_test_wrapped_bad_signature(i, 'hello') def test_kwargs(): """ Tests that we support a case where there are variable-length arguments in the decorator signature in flat mode. """ @function_decorator def foo_deco(fixture_func=DECORATED, **kwargs): return fixture_func @foo_deco def foo(): pass foo() ����������������������������������������python-decopatch-1.4.10/tests/test_doc_disambiguation.py��������������������������������������������0000664�0000000�0000000�00000006206�14207357143�0023457�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import print_function import sys import pytest from decopatch import decorator, DECORATED @pytest.mark.parametrize('flat_mode', [False, True], ids="flat_mode={}".format) def test_no_args(capsys, flat_mode): """ Tests that the no-args example works in both modes """ if not flat_mode: @decorator def replace_with_hello(): """ Decorator to replace anything with the 'hello' string. """ def _apply_decorator(f): return 'hello' return _apply_decorator else: @decorator def replace_with_hello(f=DECORATED): """ Decorator to replace anything with the 'hello' string. """ return 'hello' with capsys.disabled(): help(replace_with_hello) help(replace_with_hello) with capsys.disabled(): @replace_with_hello def foo(): pass assert foo == 'hello' @replace_with_hello() def foo(): pass assert foo == 'hello' captured = capsys.readouterr() assert captured.err == "" assert captured.out == """Help on function replace_with_hello in module tests.test_doc_disambiguation: replace_with_hello(*_) Decorator to replace anything with the 'hello' string. """ with capsys.disabled(): assert replace_with_hello(print) == 'hello' with pytest.raises(TypeError): replace_with_hello(1) with pytest.raises(TypeError): replace_with_hello(1, 2) @pytest.mark.parametrize('flat_mode', [False, True], ids="flat_mode={}".format) def test_mandatory_kwargs(flat_mode): """ Tests that the no-args example works in both modes """ if sys.version_info < (3, 0): pytest.skip("test skipped in python 2.x because kw-only is not syntactically correct") else: from ._test_doc_py3 import create_test_doc_disambiguation_kwonly_mandatory replace_with = create_test_doc_disambiguation_kwonly_mandatory(flat_mode) help(replace_with) @replace_with(replacement='hello') def foo(): pass assert foo == 'hello' with pytest.raises(TypeError): replace_with(1) with pytest.raises(TypeError): replace_with(print) @pytest.mark.parametrize('flat_mode', [False, True], ids="flat_mode={}".format) def test_optional_kwargs(flat_mode): """ Tests that the no-args example works in both modes """ if sys.version_info < (3, 0): pytest.skip("test skipped in python 2.x because kw-only is not syntactically correct") else: from ._test_doc_py3 import create_test_doc_disambiguation_kwonly_optional replace_with = create_test_doc_disambiguation_kwonly_optional(flat_mode) help(replace_with) @replace_with def foo(): pass assert foo == 'hello' @replace_with(replacement='foo') def foo(): pass assert foo == 'foo' with pytest.raises(TypeError): replace_with(1) # TypeError: replace_with() takes 0 positional arguments but 1 positional argument (and 1 keyword-only argument) were given # problem... replace_with(print) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/tests/test_introspection.py�������������������������������������������������0000664�0000000�0000000�00000002064�14207357143�0022531�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import pytest from decopatch.utils_disambiguation import disambiguate_using_introspection, FirstArgDisambiguation, \ SUPPORTS_INTROSPECTION @pytest.mark.skipif(not SUPPORTS_INTROSPECTION, reason="not available on python 3.8+") def test_on_functions(): def level1(arg): return disambiguate_using_introspection(3, arg) def my_decorator(arg): my_decorator.res = level1(arg) if my_decorator.res is FirstArgDisambiguation.is_decorated_target: return "replacement" elif my_decorator.res is FirstArgDisambiguation.is_normal_arg: def _apply(f): return "replacement" return _apply else: raise Exception() # trying to use the trick to see if that perturbates the introspection # my_decorator.__wrapped__ = level1 @my_decorator def foo(): pass assert my_decorator.res == FirstArgDisambiguation.is_decorated_target @my_decorator(foo) def foo(): pass assert my_decorator.res == FirstArgDisambiguation.is_normal_arg ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/tests/test_introspection_base.py��������������������������������������������0000664�0000000�0000000�00000003350�14207357143�0023522�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import sys import pytest from decopatch import FirstArgDisambiguation from decopatch.utils_disambiguation import disambiguate_using_introspection, SUPPORTS_INTROSPECTION def generate_decorator(): def my_deco(*args): def apply(f): my_deco.success = True return f if len(args) == 1 \ and disambiguate_using_introspection(2, args[0]) is FirstArgDisambiguation.is_decorated_target: assert args[0].__name__.lower() == 'foo' return apply(args[0]) else: return apply my_deco.success = False return my_deco NO_PARENTHESIS = 0 EMPTY_PARENTHESIS = 1 ARGS_IN_PARENTHESIS = 2 @pytest.mark.skipif(not SUPPORTS_INTROSPECTION, reason="not available on python 3.8+") @pytest.mark.parametrize('is_class', [False, True], ids="isclass={}".format) @pytest.mark.parametrize('call_mode', [NO_PARENTHESIS, EMPTY_PARENTHESIS, ARGS_IN_PARENTHESIS], ids="call_mode={}".format) def test_introspection(is_class, call_mode): my_deco = generate_decorator() if call_mode is NO_PARENTHESIS: if is_class: @my_deco class Foo: pass else: @my_deco def foo(): pass elif call_mode is EMPTY_PARENTHESIS: if is_class: @my_deco() class Foo: pass else: @my_deco() def foo(): pass elif call_mode is ARGS_IN_PARENTHESIS: if is_class: @my_deco(generate_decorator) class Foo: pass else: @my_deco(generate_decorator) def foo(): pass assert my_deco.success ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/tests/test_issues.py��������������������������������������������������������0000664�0000000�0000000�00000004045�14207357143�0021145�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import sys import pytest from decopatch import class_decorator, DECORATED, function_decorator @pytest.mark.parametrize("position", [0, 1], ids="position={}".format) def test_notnamed_flat_mode_varpositional(position): """Tests that issue https://github.com/smarie/python-decopatch/issues/12 is fixed""" if position == 0: @class_decorator def my_decorator(cls=DECORATED, *varpos): assert varpos == ("hello", 12) return cls else: @class_decorator def my_decorator(arg1, cls=DECORATED, *varpos): assert arg1 == "hello" assert varpos == (12, ) return cls @my_decorator("hello", 12) class Foo: pass @pytest.mark.parametrize("position", [0, 1], ids="position={}".format) def test_named_flat_mode_varpositional(position): """Tests that issue https://github.com/smarie/python-decopatch/issues/12 is fixed""" if position == 0: @class_decorator(flat_mode_decorated_name='cls') def my_decorator(cls, arg1, *varpos): assert arg1 == "hello" assert varpos == (12,) return cls else: @class_decorator(flat_mode_decorated_name='cls') def my_decorator(arg1, cls, *varpos): assert arg1 == "hello" assert varpos == (12, ) return cls @my_decorator("hello", 12) class Foo: pass def test_disambiguation_during_binding(): @function_decorator def my_decorator(a, *args, **kwargs): def apply(f): return f return apply @my_decorator("hello", 12) class Foo: pass def test_varpositional(): if sys.version_info < (3, 0): pytest.skip("test skipped in python 2.x because syntax is not compliant") else: from ._test_issues_py3 import create_test_varpositional replace_with = create_test_varpositional() @replace_with(1, 2, 3) class Foo(object): pass # check that Foo has been replaced with (1, 2, 3) assert len(Foo) == 3 �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/tests/test_main2.py���������������������������������������������������������0000664�0000000�0000000�00000050152�14207357143�0020640�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import print_function import sys from copy import copy from enum import Enum import pytest from pytest_cases import parametrize, parametrize_with_cases, case, fixture from decopatch import decorator, InvalidMandatoryArgError from decopatch.utils_disambiguation import SUPPORTS_INTROSPECTION from .test_main2_parametrizers import case_no_parenthesis, case_empty_parenthesis, foo, \ case_one_arg_positional_callable, case_one_arg_positional_noncallable, case_one_arg_positional_noncallable_default, \ case_one_kwarg_callable, case_one_kwarg_noncallable, \ case_one_kwarg_noncallable_default, is_foo_or_goo, case_two_args_positional_callable_first, \ case_two_args_positional_callable_last, case_two_args_positional_callable_first_dummy_default, \ case_two_args_positional_callable_last_dummy_default, goo, DEFAULT_DUMMY_VALUE try: # python 3.3+ from inspect import signature funcsigs_used = False except ImportError: from funcsigs import signature funcsigs_used = True # ------------- class NotADecoratorError(Exception): """Error raised my our checker after the decorator has been created, if it happens to not be a decorator. """ pass class codes(Enum): success = 0 skip = 1 SUCCESS = codes.success SKIP = codes.skip @fixture @parametrize_with_cases("p", cases=".test_main2_parametrizers") def decorator_application_scenario(p): return p @fixture def application_case_func(decorator_application_scenario, current_cases): """Returns the case function used behind `decorator_application_scenario`.""" return current_cases["decorator_application_scenario"]["p"].func @parametrize(flat_kw_only=[False, True]) def case_easy_0_args(decorator_application_scenario, flat_kw_only, application_case_func): """ This decorator has no arguments and can therefore be used with and without parenthesis """ if not flat_kw_only: # Note: we will decorate it later, otherwise the get_args_info will not be accurate in this particular case @decorator def replace_by_foo(): def _apply(f): return foo return _apply else: if sys.version_info < (3, 0): pytest.skip("requires python3 or higher") else: # only do it if we are in the appropriate python version from decopatch import DECORATED evaldict = copy(globals()) evaldict.update(locals()) exec(""" @decorator def replace_by_foo(*, f=DECORATED): return foo """, evaldict) replace_by_foo = evaldict['replace_by_foo'] # get_args_info(replace_by_foo), expected = {case_no_parenthesis: SUCCESS, case_empty_parenthesis: SUCCESS, case_one_arg_positional_noncallable: (TypeError, "we correctly disambiguate by default since the " "argument is non-callable"), case_one_arg_positional_noncallable_default: (TypeError, "we correctly disambiguate by default since " "the argument is non-callable"), case_one_arg_positional_callable: (NotADecoratorError, "We are not able to disambiguate but hopefully " "users will realize"), } default_value = (TypeError, "python does not allow our decorator to be called with more than 1 positional, ") # breakpoints placeholder # if application_case_func is case_one_arg_positional_callable: # print() return replace_by_foo, expected.get(application_case_func, default_value) @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires python3 or higher") def case_hard_varpositional(decorator_application_scenario, current_cases): # only do it if we are in the appropriate python version evaldict = copy(globals()) evaldict.update(locals()) exec(""" @decorator(enable_stack_introspection=False) def replace_by_foo(*args): def _apply(f): # tolerant to any order of arguments: 'goo' will be returned if found for a in args: if a is goo: return a return foo return _apply """, evaldict) replace_by_foo = evaldict['replace_by_foo'] # common expected errors expected = { # not protected: by default case_one_arg_positional_callable: (NotADecoratorError, "We are not able to disambiguate but hopefully " "users will realize"), case_one_kwarg_callable: (TypeError, "decorator impl does not accept keyword args"), case_one_kwarg_noncallable: (TypeError, "decorator impl does not accept keyword args"), case_one_kwarg_noncallable_default: (TypeError, "decorator impl does not accept keyword args"), } application_case_func = current_cases["decorator_application_scenario"]["p"].func # if application_case_func in {case_no_parenthesis, case_one_arg_positional_callable}: # print() default_value = SUCCESS return replace_by_foo, expected.get(application_case_func, default_value) @parametrize(protection=[ 'default', pytest.param('introspection', marks=pytest.mark.skipif(not SUPPORTS_INTROSPECTION, reason="not available on python 3.8+")), ]) def case_hard_1_m_0_opt_noncallable(decorator_application_scenario, protection, current_cases): """ This decorator has 1 mandatory argument. It has therefore a possible ambiguity when called without parenthesis """ use_introspection = (protection == 'introspection') @decorator(enable_stack_introspection=use_introspection) def replace_by_foo(dummy): def _apply(f): return foo return _apply # common expected errors expected = { case_no_parenthesis: (InvalidMandatoryArgError, "a no-parenthesis usage will be declared by the " "default disambiguator or the stack introspecter " "as decorated target"), case_empty_parenthesis: (TypeError, "python does not allow f() if f has 1 mandatory arg"), # case_one_arg_positional_callable: case_one_arg_positional_noncallable: SUCCESS, case_one_arg_positional_noncallable_default: SUCCESS, case_one_kwarg_callable: (SKIP, "decorator impl does not have a 'replacement' arg"), case_one_kwarg_noncallable: SUCCESS, case_one_kwarg_noncallable_default: SUCCESS, } if not use_introspection: expected.update({case_one_arg_positional_callable: (InvalidMandatoryArgError, "calling with a single positional callable" "will be declared by the default disambiguator or " "the stack introspecter as decorated target, and" "therefore it will say that there is something " "missing"),}) else: expected.update({case_one_arg_positional_callable: (AssertionError, "The stack introspector will work " "correctly. So the decorated function" "will be replaced by foo. Which is not" "`goo`"),}) application_case_func = current_cases["decorator_application_scenario"]["p"].func # if use_introspection and application_case_func is case_one_arg_positional_callable: # print() default_value = (TypeError, "python does not allow 2 args if f has 1 arg") return replace_by_foo, expected.get(application_case_func, default_value) @parametrize(protection=['protected(default)', 'protected(explicit)'], kw_only=[False, True]) def case_hard_1_m_0_opt_callable(decorator_application_scenario, protection, kw_only, current_cases): """This decorator has 1 mandatory argument. It has therefore a possible ambiguity when called without parenthesis""" protected_explicit = (protection == 'protected(explicit)') # the decorator impl def replace_by_foo(replacement): def _apply(f): return replacement return _apply if not protected_explicit: # as usual, but manually replace_by_foo = decorator()(replace_by_foo) else: # we protect it by saying that # - first argument should be one of {foo, goo}, # - and that otherwise it is sure that it is a no-parenthesis call replace_by_foo = decorator(custom_disambiguator=is_foo_or_goo)(replace_by_foo) # common expected errors expected = { # case_no_parenthesis case_empty_parenthesis: (TypeError, "python does not allow f() if f has 1 mandatory arg"), # case_one_arg_positional_callable case_one_arg_positional_noncallable: (SKIP, "This decorator does not access a noncallable positional arg"), case_one_arg_positional_noncallable_default: (SKIP, "This decorator does not access a noncallable positional " "arg"), # case_one_kwarg_callable case_one_kwarg_noncallable: (SKIP, "decorator impl does not have a 'dummy' arg"), case_one_kwarg_noncallable_default: (SKIP, "decorator impl does not have a 'dummy' arg"), } # errors that protection changes if not protected_explicit: # note : when can_first_arg_be_ambiguous=True the errors would be 'AmbiguousFirstArgumentTypeError' expected.update({ case_no_parenthesis: (InvalidMandatoryArgError, "using the decorator without parenthesis mimics " "usage with a single arg."), case_one_arg_positional_callable: (InvalidMandatoryArgError, "calling a decorator with a callable as first " "and only non-default argument leads by " "default to an error"), # with the signature trick it is ok : case_one_kwarg_callable: SUCCESS, }) if funcsigs_used: # no signature trick ! expected.update({case_one_kwarg_callable: (InvalidMandatoryArgError, "calling a decorator with a callable as first " "and only non-default argument leads by " "default to an error"), }) else: expected.update({ case_no_parenthesis: (InvalidMandatoryArgError, "a no-parenthesis usage will be declared by the " "disambiguator as decorated target"), case_one_arg_positional_callable: SUCCESS, case_one_kwarg_callable: SUCCESS, }) default_value = (TypeError, "python does not allow 2 args if f has 1 arg") application_case_func = current_cases["decorator_application_scenario"]["p"].func return replace_by_foo, expected.get(application_case_func, default_value) @case(id="easy_2m_args(replacement, dummy)") def case_easy_2_m_0_opt_callable_first(decorator_application_scenario, current_cases): @decorator def replace_by_foo(replacement, dummy): def _apply(f): return replacement return _apply # get_args_info(replace_by_foo), expected = { case_two_args_positional_callable_first: SUCCESS, case_two_args_positional_callable_last: (SKIP, "the order of positional args in the test does not match"), case_two_args_positional_callable_first_dummy_default: SUCCESS, case_two_args_positional_callable_last_dummy_default: (SKIP, "the order of positional args in the test does not match"), } default_value = (TypeError, "python does not allow < 2 args if f has 2 arg") application_case_func = current_cases["decorator_application_scenario"]["p"].func return replace_by_foo, expected.get(application_case_func, default_value) @case(id="easy_2m_args(dummy, replacement)") def case_easy_2_m_0_opt_callable_last(decorator_application_scenario, application_case_func): @decorator def replace_by_foo(dummy, replacement): def _apply(f): return replacement return _apply # get_args_info(replace_by_foo), expected = { case_two_args_positional_callable_first: (SKIP, "the order of positional args in the test does not match"), case_two_args_positional_callable_last: SUCCESS, case_two_args_positional_callable_first_dummy_default: (SKIP, "the order of positional args in the test does not match"), case_two_args_positional_callable_last_dummy_default: SUCCESS } default_value = (TypeError, "python does not allow < 2 args if f has 2 arg") return replace_by_foo, expected.get(application_case_func, default_value) @case(id="easy_2m_args(dummy, dummy2)") def case_easy_2_m_0_opt_no_callable(decorator_application_scenario, application_case_func): @decorator def replace_by_foo(dummy, dummy2): def _apply(f): return goo return _apply # get_args_info(replace_by_foo), expected = { case_two_args_positional_callable_first: SUCCESS, case_two_args_positional_callable_last: SUCCESS, case_two_args_positional_callable_first_dummy_default: SUCCESS, case_two_args_positional_callable_last_dummy_default: SUCCESS, } default_value = (TypeError, "python does not allow < 2 args if f has 2 arg") return replace_by_foo, expected.get(application_case_func, default_value) @parametrize(protection=[ 'default', pytest.param('introspection', marks=pytest.mark.skipif(not SUPPORTS_INTROSPECTION, reason="not available on python 3.8+")), ]) def case_hard_0_m_2_opt_callable_last(decorator_application_scenario, protection, application_case_func): use_introspection = (protection == 'introspection') @decorator(enable_stack_introspection=use_introspection) def replace_by_foo(dummy=DEFAULT_DUMMY_VALUE, replacement=None): def _apply(f): return replacement if replacement is not None else foo return _apply # common expected errors expected = { case_one_arg_positional_callable: (SKIP, "the first positional arg is supposed not to be the callable here"), case_two_args_positional_callable_first: (SKIP, "the order of positional args in the test does not match"), case_two_args_positional_callable_first_dummy_default: (SKIP, "the order of positional args in the test does " "not match"), } # if protected and application_case_func is case_no_parenthesis: # print() default_value = SUCCESS return replace_by_foo, expected.get(application_case_func, default_value) @parametrize(protection=[ 'unprotected (default)', pytest.param('introspection', marks=pytest.mark.skipif(not SUPPORTS_INTROSPECTION, reason="not available on python 3.8+")), 'custom_disambiguator' ]) def case_hard_0_m_2_opt_callable_first(decorator_application_scenario, protection, application_case_func): use_introspection = (protection == 'introspection') use_custom = (protection == 'custom_disambiguator') is_default = not use_introspection and not use_custom # the decorator impl def replace_by_foo(replacement=None, dummy=DEFAULT_DUMMY_VALUE): def _apply(f): return replacement if replacement is not None else foo return _apply if not use_custom: # as usual but manually replace_by_foo = decorator(enable_stack_introspection=use_introspection)(replace_by_foo) else: # we protect it by saying that first arg should be either foo or goo replace_by_foo = decorator(custom_disambiguator=is_foo_or_goo)(replace_by_foo) # common expected errors expected = { case_one_arg_positional_noncallable: (SKIP, "the first positional arg is supposed to be the callable here"), case_one_arg_positional_noncallable_default: (SKIP, "the first positional arg is supposed to be the callable here"), case_two_args_positional_callable_last: (SKIP, "the order of positional args in the test does not match"), case_two_args_positional_callable_last_dummy_default: (SKIP, "the order of positional args in the test does not match"), } # errors that protection changes if is_default: expected.update({ case_one_arg_positional_callable: (NotADecoratorError, "No explicit exception is raised but since a " "double-call is made, user will probably realize " "that something is wrong"), }) if funcsigs_used: # no signature trick ! expected.update({ case_one_kwarg_callable: (NotADecoratorError, "No explicit exception is raised but since a double-call is " "made, user will probably realize that something is wrong"), case_two_args_positional_callable_first_dummy_default: (NotADecoratorError, "No explicit exception is " "raised but since a double-call" " is made, user will probably " "realize that something is " "wrong"), }) default_value = SUCCESS # this is how you can put breakpoints if use_introspection and application_case_func is case_one_arg_positional_callable: print() return replace_by_foo, expected.get(application_case_func, default_value) # ------------------------- @parametrize_with_cases("replace_by_foo, expected_err", cases=".") def test_all(replace_by_foo, expected_err, decorator_application_scenario, application_case_func): # get the decorator factory, and the associated expected outcome print("Generated decorator : %s%s" % (replace_by_foo.__name__, signature(replace_by_foo))) print("Calling it as %s" % application_case_func.__name__) if expected_err is SUCCESS: print("Expected SUCCESS\n") execute_nominal_test(replace_by_foo, decorator_application_scenario) else: # unpack the expected error and detect SKIP scenario expected_err_type, expected_failure_msg = expected_err if expected_err_type is SKIP: pytest.skip(expected_failure_msg) # execute and assert that error of correct type is raised print("Expected ERROR: '%s' because %s\n" % (expected_err_type.__name__, expected_failure_msg)) with pytest.raises(expected_err_type): execute_nominal_test(replace_by_foo, decorator_application_scenario) def execute_nominal_test(replace_by_foo, decorator_application_scenario): """ The actual test code: we create a decorator, check it, and apply it. :param replace_by_foo: :param decorator_application_scenario: :return: """ created_decorator = decorator_application_scenario[0](replace_by_foo) expected_replacement = decorator_application_scenario[1] if not callable(created_decorator) or created_decorator in {foo, goo, DEFAULT_DUMMY_VALUE}: # this is not even a decorator, that's directly an object # this happens when the stack mistakenly thought that the decorator was used without argument, while it # was used with argument. raise NotADecoratorError("created decorator is not a decorator: it is %s" % created_decorator) @created_decorator def bar(): pass assert bar is expected_replacement ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/tests/test_main2_parametrizers.py�������������������������������������������0000664�0000000�0000000�00000012401�14207357143�0023603�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import pytest from decopatch import FirstArgDisambiguation from pytest_cases import case try: # python 3.3+ from inspect import signature, Parameter except ImportError: from funcsigs import signature, Parameter def foo(): pass def goo(): pass def is_foo_or_goo(arg): if arg in {foo, goo}: return FirstArgDisambiguation.is_normal_arg else: return FirstArgDisambiguation.is_decorated_target DEFAULT_DUMMY_VALUE = 12 @case(id="@replace_by_foo") def case_no_parenthesis(): """ Tests the decorator in a no-parenthesis way @my_decorator """ return lambda replace_by_foo: replace_by_foo, foo @case(id="@replace_by_foo()") def case_empty_parenthesis(): """ Tests the decorator in a with-empty-parenthesis way @my_decorator() """ return lambda replace_by_foo: replace_by_foo(), foo @case(id="@replace_by_foo(goo)") def case_one_arg_positional_callable(): """ Tests the decorator with one positional argument @my_decorator(goo) """ return lambda replace_by_foo: replace_by_foo(goo), goo @case(id="@replace_by_foo('hello')") def case_one_arg_positional_noncallable(): """ Tests the decorator with one positional argument @my_decorator("hello") """ return lambda replace_by_foo: replace_by_foo('hello'), foo @case(id="@replace_by_foo(DEFAULT_DUMMY_VALUE)") def case_one_arg_positional_noncallable_default(): """ Tests the decorator with one positional argument @my_decorator(DEFAULT_DUMMY_VALUE) """ return lambda replace_by_foo: replace_by_foo(DEFAULT_DUMMY_VALUE), foo @case(id="@replace_by_foo(replacement=goo)") def case_one_kwarg_callable(): """ Tests the decorator with one kw argument @my_decorator(replacement=goo) """ return lambda replace_by_foo: replace_by_foo(replacement=goo), goo @case(id="@replace_by_foo(dummy='hello')") def case_one_kwarg_noncallable(): """ Tests the decorator with one kw argument @my_decorator(dummy="hello") """ return lambda replace_by_foo: replace_by_foo(dummy='hello'), foo @case(id="@replace_by_foo(dummy=DEFAULT_DUMMY_VALUE)") def case_one_kwarg_noncallable_default(): """ Tests the decorator with one kw argument @my_decorator(dummy=DEFAULT_DUMMY_VALUE) """ return lambda replace_by_foo: replace_by_foo(dummy=DEFAULT_DUMMY_VALUE), foo @case(id="@replace_by_foo(goo, 'hello')") def case_two_args_positional_callable_first(): """ Tests the decorator with one positional argument @my_decorator(goo) """ return lambda replace_by_foo: replace_by_foo(goo, 'hello'), goo @case(id="@replace_by_foo('hello', goo)") def case_two_args_positional_callable_last(): """ Tests the decorator with one positional argument @my_decorator(goo) """ return lambda replace_by_foo: replace_by_foo('hello', goo), goo @case(id="@replace_by_foo(goo, DEFAULT_DUMMY_VALUE)") def case_two_args_positional_callable_first_dummy_default(): """ Tests the decorator with one positional argument @my_decorator(goo) """ return lambda replace_by_foo: replace_by_foo(goo, DEFAULT_DUMMY_VALUE), goo @case(id="@replace_by_foo(DEFAULT_DUMMY_VALUE, goo)") def case_two_args_positional_callable_last_dummy_default(): """ Tests the decorator with one positional argument @my_decorator(goo) """ return lambda replace_by_foo: replace_by_foo(DEFAULT_DUMMY_VALUE, goo), goo # @pytest.mark.parametrize('new_val_for_dummy', ['hello', DEFAULT_DUMMY_VALUE]) # def case_two_args_kw_both_orders(case_data, new_val_for_dummy): # """ Tests the decorator in a with-empty-parenthesis way @my_decorator() """ # # replace_by_foo, args_info, is_first_arg_protected = case_data.get() # help(replace_by_foo) # print(args_info) # # # why would this test fail ? # expected_failure_msg = None # if not args_info.has_dummy_kwarg: # expected_failure_msg = "'dummy' kw arg is not present in the signature" # expected_err_type = TypeError # elif not args_info.has_replacement_kwarg: # expected_failure_msg = "'replacement' kw arg is not present in the signature" # expected_err_type = TypeError # elif new_val_for_dummy is DEFAULT_DUMMY_VALUE \ # and args_info.first_arg_name == "replacement" and not is_first_arg_protected: # # note: is new_val_for_dummy is DEFAULT_DUMMY_VALUE, then the stack will not be able to know that 2 arguments # # were provided. # expected_failure_msg = "this is an ambiguous case and the first argument is not protected" # expected_err_type = AmbiguousFirstArgumentTypeError # # if expected_failure_msg is None: # print("this is supposed to work") # # @replace_by_foo(dummy=new_val_for_dummy, replacement=goo) # def bar(): # pass # # assert bar is goo # # @replace_by_foo(replacement=goo, dummy=new_val_for_dummy) # def bar(): # pass # # assert bar is goo # # else: # print("this is supposed to raise a '%s' because %s" % (expected_err_type, expected_failure_msg)) # # with pytest.raises(expected_err_type): # @replace_by_foo(dummy=new_val_for_dummy, replacement=goo) # def bar(): # pass # # with pytest.raises(expected_err_type): # @replace_by_foo(replacement=goo, dummy=new_val_for_dummy) # def bar(): # pass ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-decopatch-1.4.10/tests/test_so.py������������������������������������������������������������0000664�0000000�0000000�00000031644�14207357143�0020260�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import print_function import logging import pytest try: # python 3.3+ from inspect import signature, Parameter except ImportError: from funcsigs import signature, Parameter from decopatch import function_decorator, WRAPPED, F_ARGS, F_KWARGS, DECORATED from makefun import wraps, add_signature_parameters, remove_signature_parameters def test_so_flat(): """ Tests that answer at https://stackoverflow.com/questions/28693930/when-to-use-decorator-and-decorator-factory/55078327#55078327 is working """ @function_decorator def before_run(func=DECORATED): @wraps(func) def _execute(*f_args, **f_kwargs): print("hello from before run") if f_kwargs['a'] > 0: f_kwargs['a'] = 100 return func(*f_args, **f_kwargs) return _execute @before_run def running_func(a, b): print("a", a, "b", b) return a + b assert running_func(-1, 2) == 1 assert running_func(1, 2) == 102 def test_so_double_flat(): """ Tests that answer at https://stackoverflow.com/questions/28693930/when-to-use-decorator-and-decorator-factory/55078327#55078327 is working """ @function_decorator def before_run(func=WRAPPED, f_args=F_ARGS, f_kwargs=F_KWARGS): print("hello from before run") if f_kwargs['a'] > 0: f_kwargs['a'] = 100 return func(*f_args, **f_kwargs) @before_run def running_func(a, b): print("a", a, "b", b) return a + b assert running_func(-1, 2) == 1 assert running_func(1, 2) == 102 @pytest.mark.parametrize('style', ['flat', 'double-flat'], ids="style={}".format) def test_so_wrap(style): """ Tests that the answer at https://stackoverflow.com/a/55105198/7262247 works correctly :return: """ if style == 'flat': @function_decorator def makestyle(style='b', fn=DECORATED): open_tag = "<%s>" % style close_tag = "</%s>" % style @wraps(fn) def wrapped(*args, **kwargs): return open_tag + fn(*args, **kwargs) + close_tag return wrapped else: @function_decorator def makestyle(style='b', fn=WRAPPED, f_args=F_ARGS, f_kwargs=F_KWARGS): open_tag = "<%s>" % style close_tag = "</%s>" % style return open_tag + fn(*f_args, **f_kwargs) + close_tag @makestyle @makestyle('i') def hello(who): return "hello %s" % who assert hello('world') == '<b><i>hello world</i></b>' # you can check that with pytest.raises(TypeError): hello() # TypeError: hello() missing 1 required positional argument: 'who' @pytest.mark.parametrize('mode', ['flat', 'double-flat'], ids="mode={}".format) def test_so_2(mode): """ Tests that the solution posted at https://stackoverflow.com/questions/52126071/decorator-with-arguments-avoid-parenthesis-when-no-arguments/55106984#55106984 Actually works """ if mode == 'flat': @function_decorator def logged(disabled=False, logger=logging.getLogger('default'), func=DECORATED): # (1) create a signature-preserving wrapper @wraps(func) def _func_wrapper(*f_args, **f_kwargs): # stuff result = func(*f_args, **f_kwargs) # stuff return result # (2) return it return _func_wrapper else: @function_decorator def logged(disabled=False, logger=logging.getLogger('default'), func=WRAPPED, f_args=F_ARGS, f_kwargs=F_KWARGS): # this is directly the signature-preserving wrapper # stuff result = func(*f_args, **f_kwargs) # stuff return result @logged(disabled=True) def foo(): pass @logged def bar(): pass foo() bar() @pytest.mark.parametrize('mode', ['flat', 'double-flat'], ids="mode={}".format) def test_so_3(mode): """ Checks that the answer at https://stackoverflow.com/a/55107188/7262247 works correctly :param mode: :return: """ if mode == 'flat': @function_decorator def foo_register(method_name=None, method=DECORATED): if method_name is None: method.gw_method = method.__name__ else: method.gw_method = method_name # create a signature-preserving wrapper @wraps(method) def wrapper(*args, **kwargs): method(*args, **kwargs) return wrapper else: @function_decorator def foo_register(method_name=None, method=WRAPPED, f_args=F_ARGS, f_kwargs=F_KWARGS): # this is directly the wrapper if method_name is None: method.gw_method = method.__name__ else: method.gw_method = method_name method(*f_args, **f_kwargs) @foo_register def my_function(): print('hi...') @foo_register('say_hi') def my_function(): print('hi...') @pytest.mark.parametrize('mode', [None, 'flat'], ids="mode={}".format) def test_so_4(capsys, mode): """ Checks that the answer at https://stackoverflow.com/a/55160435/7262247 works """ if mode is None: def my_decorator(decorator_arg1=None, decorator_arg2=False): # Inside the wrapper maker def _decorator(func): # (1) capture the signature of the function to wrap ... func_sig = signature(func) # ... and modify it to add new optional parameters 'new_arg1' and 'new_arg2'. # (if they are optional that's where you provide their defaults) new_arg1 = Parameter('new_arg1', kind=Parameter.POSITIONAL_OR_KEYWORD, default=False) new_arg2 = Parameter('new_arg2', kind=Parameter.POSITIONAL_OR_KEYWORD, default=None) new_sig = add_signature_parameters(func_sig, last=[new_arg1, new_arg2]) # (2) create a wrapper with the new signature @wraps(func, new_sig=new_sig) def func_wrapper(*args, **kwds): # Inside the wrapping function # Pop the extra args (they will always be there, no need to provide default) new_arg1 = kwds.pop('new_arg1') new_arg2 = kwds.pop('new_arg2') # Calling the wrapped function if new_arg1: print("new_arg1 True branch; new_arg2 is {}".format(new_arg2)) return func(*args, **kwds) else: print("new_arg1 False branch; new_arg2 is {}".format(new_arg2)) # do something with new_arg2 return func(*args, **kwds) def added_function(): # Do Something 2 print('added_function') func_wrapper.added_function = added_function return func_wrapper return _decorator else: @function_decorator def my_decorator(decorator_arg1=None, decorator_arg2=False, func=DECORATED): # (1) capture the signature of the function to wrap ... func_sig = signature(func) # ... and modify it to add new optional parameters 'new_arg1' and 'new_arg2'. # (if they are optional that's where you provide their defaults) new_arg1 = Parameter('new_arg1', kind=Parameter.POSITIONAL_OR_KEYWORD, default=False) new_arg2 = Parameter('new_arg2', kind=Parameter.POSITIONAL_OR_KEYWORD, default=None) new_sig = add_signature_parameters(func_sig, last=[new_arg1, new_arg2]) # (2) create a wrapper with the new signature @wraps(func, new_sig=new_sig) def func_wrapper(*args, **kwds): # Inside the wrapping function # Pop the extra args (they will always be there, no need to provide default) new_arg1 = kwds.pop('new_arg1') new_arg2 = kwds.pop('new_arg2') # Calling the wrapped function if new_arg1: print("new_arg1 True branch; new_arg2 is {}".format(new_arg2)) return func(*args, **kwds) else: print("new_arg1 False branch; new_arg2 is {}".format(new_arg2)) # do something with new_arg2 return func(*args, **kwds) def added_function(): # Do Something 2 print('added_function') func_wrapper.added_function = added_function return func_wrapper @my_decorator(decorator_arg1=4, decorator_arg2=True) def foo(a, b): """This is my foo function""" print("a={}, b={}".format(a,b)) foo(1, 2, True, 7) # works, except if you use kind=Parameter.KEYWORD_ONLY above (in which case wont work in python 2) foo(1, 2, new_arg1=True, new_arg2=7) foo(a=3, b=4, new_arg1=False, new_arg2=42) foo(new_arg2=-1,b=100,a='AAA') foo(b=100,new_arg1=True,a='AAA') foo.added_function() help(foo) captured = capsys.readouterr() with capsys.disabled(): print(captured.out) assert captured.out == """new_arg1 True branch; new_arg2 is 7 a=1, b=2 new_arg1 True branch; new_arg2 is 7 a=1, b=2 new_arg1 False branch; new_arg2 is 42 a=3, b=4 new_arg1 False branch; new_arg2 is -1 a=AAA, b=100 new_arg1 True branch; new_arg2 is None a=AAA, b=100 added_function Help on function foo in module tests.test_so: foo(a, b, new_arg1=False, new_arg2=None) This is my foo function """ @pytest.mark.parametrize('mode', ['no-deco', None, 'flat'], ids="mode={}".format) def test_so_5(capsys, mode): """ Tests that the solution at https://stackoverflow.com/a/55161579/7262247 works """ if mode == 'no-deco': def make_test(a, b): """A factory to create the test function in various flavours according to meta-parameters (a, b)""" def test(x, y): print(a, b) print(x, y) # here you could play around with the created functions' identity if needed # test.__name__ = ... # test.__qualname__ = ... # test.__module__ = ... # test.__dict__ = ... return test test = make_test(a='hello', b='world') else: if mode is None: def more_vars(**extras): def wrapper(f): # (1) capture the signature of the function to wrap and remove the invisible func_sig = signature(f) new_sig = remove_signature_parameters(func_sig, 'invisible_args') # (2) create a wrapper with the new signature @wraps(f, new_sig=new_sig) def wrapped(*args, **kwargs): # inject the invisible args again kwargs['invisible_args'] = extras return f(*args, **kwargs) return wrapped return wrapper else: @function_decorator def more_vars(f=DECORATED, **extras): # (1) capture the signature of the function to wrap and remove the invisible func_sig = signature(f) new_sig = remove_signature_parameters(func_sig, 'invisible_args') # (2) create a wrapper with the new signature @wraps(f, new_sig=new_sig) def wrapped(*args, **kwargs): kwargs['invisible_args'] = extras return f(*args, **kwargs) return wrapped @more_vars(a='hello', b='world') def test(x, y, invisible_args): a = invisible_args['a'] b = invisible_args['b'] print(a, b) print(x, y) test(1, 2) help(test) captured = capsys.readouterr() with capsys.disabled(): print(captured.out) assert captured.out == """hello world 1 2 Help on function test in module tests.test_so: test(x, y) """ def test_so_6(capsys): """ Tests that the answer at https://stackoverflow.com/a/55163391/7262247 is correct """ @function_decorator def my_decorator(name='my_decorator', func=DECORATED): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) wrapper._decorator_name_ = name 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 """ ��������������������������������������������������������������������������������������������python-decopatch-1.4.10/tests/test_typing.py��������������������������������������������������������0000664�0000000�0000000�00000003226�14207357143�0021144�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This is test file for typing, # No automatic testing is used at the moment. Just use your type checker and see if it works. # Pytest here is used to make sure that runtime behavir matches with type checker expecter errors. from typing import Any, Callable import pytest from decopatch import DECORATED, function_decorator def test_invalid_parameter(): with pytest.raises(TypeError): # Error, invalid argument @function_decorator(invalid_param=True) def decorator_wint_invalid_param(fn=DECORATED): return fn def test_normal_decorator(): @function_decorator def decorator(scope="test", fn=DECORATED): # type: (str, Any) -> Callable[..., Any] assert isinstance(scope, str) return fn # Ok @decorator def decorated_flat(): pass assert decorated_flat with pytest.raises(AssertionError): # Error, Literal[2] is incompatible with str @decorator(scope=2) def decorated_with_invalid_options(): pass # Ok, should reveal correct type for `scope` @decorator(scope="success") def decorated_with_valid_options(): pass assert decorated_with_valid_options def test_function_decorator_with_params(): # Ok, should reveal correct type for `enable_stack_introspection` @function_decorator(enable_stack_introspection=True) def decorator_with_params(scope = "test", fn=DECORATED): # type: (str, Any) -> Callable[..., Any] return fn # Ok, should reveal correct type for `scope` @decorator_with_params(scope="success") def decorated_with_valid_options(): pass assert decorated_with_valid_options ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������