././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1633641436.399064 apipkg-2.1.0/0000755000175100001710000000000000000000000012325 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1633641436.391064 apipkg-2.1.0/.github/0000755000175100001710000000000000000000000013665 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1633641436.3950639 apipkg-2.1.0/.github/workflows/0000755000175100001710000000000000000000000015722 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641425.0 apipkg-2.1.0/.github/workflows/pre-commit.yml0000644000175100001710000000034700000000000020525 0ustar00runnerdockername: pre-commit on: pull_request: push: branches: [main] jobs: pre-commit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - uses: pre-commit/action@v2.0.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641425.0 apipkg-2.1.0/.github/workflows/pythonapp.yml0000644000175100001710000000445100000000000020473 0ustar00runnerdockername: "python tests+artifacts+release" on: pull_request: push: branches: - main tags: - "v*" release: types: [published] jobs: dist: runs-on: ubuntu-latest name: Python sdist/wheel steps: - uses: actions/checkout@v1 - uses: actions/setup-python@v2 with: python-version: "3.9" - name: Install dependencies run: | python -m pip install --upgrade pip pip install --upgrade wheel setuptools build - name: Build package run: python -m build -o dist/ - uses: actions/upload-artifact@v2 with: name: dist path: dist test: runs-on: ubuntu-latest needs: [dist] strategy: matrix: # todo: extract from source python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, "3.10-dev"] install-from: ["dist/*.whl"] include: - python-version: 3.9 install-from: "-e ." - python-version: 3.9 install-from: "." - python-version: 3.9 install-from: "dist/*.tar.gz" steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -U setuptools setuptools_scm pip install pytest - uses: actions/download-artifact@v2 with: name: dist path: dist - name: install editable run: pip install ${{ matrix.install-from }} - name: pytest run: pytest dist_check: runs-on: ubuntu-latest needs: [dist] steps: - uses: actions/setup-python@v2 with: python-version: "3.9" - uses: actions/download-artifact@v2 with: name: dist path: dist - run: pipx run twine check --strict dist/* dist_upload: runs-on: ubuntu-latest if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') needs: [dist_check, test] steps: - uses: actions/download-artifact@v2 with: name: dist path: dist - name: Publish package to PyPI uses: pypa/gh-action-pypi-publish@master with: user: __token__ password: ${{ secrets.pypi_token }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641425.0 apipkg-2.1.0/.gitignore0000644000175100001710000000011100000000000014306 0ustar00runnerdockersrc/apipkg/version.py __pycache__ *.egg-info .pytest_cache/ .eggs/ .tox/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641425.0 apipkg-2.1.0/.pre-commit-config.yaml0000644000175100001710000000177500000000000016620 0ustar00runnerdockerrepos: - repo: https://github.com/psf/black rev: 20.8b1 hooks: - id: black args: [--safe, --quiet] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: fix-encoding-pragma args: [--remove] - id: check-yaml - id: debug-statements language_version: python3 - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.4 hooks: - id: flake8 language_version: python3 additional_dependencies: - flake8-typing-imports==1.9.0 - repo: https://github.com/asottile/reorder_python_imports rev: v2.3.6 hooks: - id: reorder-python-imports args: ['--application-directories=.:src'] - repo: https://github.com/asottile/pyupgrade rev: v2.8.0 hooks: - id: pyupgrade - repo: https://github.com/asottile/setup-cfg-fmt rev: v1.16.0 hooks: - id: setup-cfg-fmt args: [--min-py3-version=3.4] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641425.0 apipkg-2.1.0/CHANGELOG0000644000175100001710000000574500000000000013552 0ustar00runnerdocker2.1.0 ---------------------------------------- - fix race condition for import of modules using apipkg.initpkg in Python 3.3+ by updating existing modules in-place rather than replacing in sys.modules with an apipkg.ApiModule instances. This race condition exists for import statements (and __import__) in Python 3.3+ where sys.modules is checked before obtaining an import lock, and for importlib.import_module in Python 3.11+ for the same reason. 2.0.1 ---------------------------------------- - fix race conditions for attribute creation 2.0.0 ---------------------------------------- - also transfer __spec__ attribute - make py.test hack more specific to avoid hiding real errors - switch from Travis CI to GitHub Actions - modernize package build - reformat code with black 1.5 ---------------------------------------- - switch to setuptools_scm - move to github - fix up python compat matrix - avoid dict iteration (fixes issue on python3) - preserve __package__ - ths gets us better pep 302 compliance 1.4 ---------------------------------------- - revert the automated version gathering 1.3 ---------------------------------------- - fix issue2 - adapt tests on Jython - handle jython __pkgpath__ missabstraction when running python from jar files - alias modules pointing to unimportable modules will return None for all their attributes instead of raising ImportError. This addresses python3.4 where any call to getframeinfo() can choke on sys.modules contents if pytest is not installed (because py.test.* imports it). - introduce apipkg.distribution_version(name) as helper to obtain the current version number of a package from install metadata its used by default with the package name - add an eagerloading option and eagerload automatically if bpython is used (workaround for their monkeypatching) 1.2 ---------------------------------------- - Allow to import from Aliasmodules (thanks Ralf Schmitt) - avoid loading __doc__ early, so ApiModule is now fully lazy 1.1 ---------------------------------------- - copy __doc__ and introduce a new argument to initpkg() (thanks Ralf Schmitt) - don't use a "0" default for __version__ 1.0 ---------------------------------------- - make apipkg memorize the absolute path where a package starts importing so that subsequent chdir + imports won't break. - allow to alias modules - allow to use dotted names in alias specifications (thanks Ralf Schmitt). 1.0.0b6 ---------------------------------------- - fix recursive import issue resulting in a superflous KeyError - default to __version__ '0' and not set __loader__ or __path__ at all if it doesn't exist on the underlying init module 1.0.0b5 ---------------------------------------- - fixed MANIFEST.in - also transfer __loader__ attribute (thanks Ralf Schmitt) - compat fix for BPython 1.0.0b3 (compared to 1.0.0b2) ------------------------------------ - added special __onfirstaccess__ attribute whose value will be called on the first attribute access of an apimodule. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641425.0 apipkg-2.1.0/LICENSE0000644000175100001710000000203600000000000013333 0ustar00runnerdocker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641425.0 apipkg-2.1.0/MANIFEST.in0000644000175100001710000000011300000000000014056 0ustar00runnerdockerinclude tox.ini include setup.cfg include CHANGELOG include test_apipkg.py ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1633641436.399064 apipkg-2.1.0/PKG-INFO0000644000175100001710000001030400000000000013420 0ustar00runnerdockerMetadata-Version: 2.1 Name: apipkg Version: 2.1.0 Summary: apipkg: namespace control and lazy-import mechanism Home-page: https://github.com/pytest-dev/apipkg Author: holger krekel Maintainer: Ronny Pfannschmidt Maintainer-email: opensource@ronnypfannschmidt.de License: MIT Platform: unix Platform: linux Platform: osx Platform: cygwin Platform: win32 Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Software Development :: Libraries Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7 Description-Content-Type: text/x-rst License-File: LICENSE Welcome to apipkg ! ------------------- With apipkg you can control the exported namespace of a Python package and greatly reduce the number of imports for your users. It is a `small pure Python module`_ that works on CPython 2.7 and 3.4+, Jython and PyPy. It cooperates well with Python's ``help()`` system, custom importers (PEP302) and common command-line completion tools. Usage is very simple: you can require 'apipkg' as a dependency or you can copy paste the ~200 lines of code into your project. Tutorial example ------------------- Here is a simple ``mypkg`` package that specifies one namespace and exports two objects imported from different modules:: # mypkg/__init__.py import apipkg apipkg.initpkg(__name__, { 'path': { 'Class1': "_mypkg.somemodule:Class1", 'clsattr': "_mypkg.othermodule:Class2.attr", } } The package is initialized with a dictionary as namespace. You need to create a ``_mypkg`` package with a ``somemodule.py`` and ``othermodule.py`` containing the respective classes. The ``_mypkg`` is not special - it's a completely regular Python package. Namespace dictionaries contain ``name: value`` mappings where the value may be another namespace dictionary or a string specifying an import location. On accessing an namespace attribute an import will be performed:: >>> import mypkg >>> mypkg.path >>> mypkg.path.Class1 # '_mypkg.somemodule' gets imported now >>> mypkg.path.clsattr # '_mypkg.othermodule' gets imported now 4 # the value of _mypkg.othermodule.Class2.attr The ``mypkg.path`` namespace and its two entries are loaded when they are accessed. This means: * lazy loading - only what is actually needed is ever loaded * only the root "mypkg" ever needs to be imported to get access to the complete functionality * the underlying modules are also accessible, for example:: from mypkg.sub import Class1 Including apipkg in your package -------------------------------------- If you don't want to add an ``apipkg`` dependency to your package you can copy the `apipkg.py`_ file somewhere to your own package, for example ``_mypkg/apipkg.py`` in the above example. You then import the ``initpkg`` function from that new place and are good to go. .. _`small pure Python module`: .. _`apipkg.py`: https://github.com/pytest-dev/apipkg/blob/master/src/apipkg/__init__.py Feedback? ----------------------- If you have questions you are welcome to * join the **#pytest** channel on irc.libera.chat_ (using an IRC client, via webchat_, or via Matrix_). * create an issue on the bugtracker_ .. _irc.libera.chat: ircs://irc.libera.chat:6697/#pytest .. _webchat: https://web.libera.chat/#pytest .. _matrix: https://matrix.to/#/%23pytest:libera.chat .. _bugtracker: https://github.com/pytest-dev/apipkg/issues ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641425.0 apipkg-2.1.0/README.rst0000644000175100001710000000556000000000000014022 0ustar00runnerdockerWelcome to apipkg ! ------------------- With apipkg you can control the exported namespace of a Python package and greatly reduce the number of imports for your users. It is a `small pure Python module`_ that works on CPython 2.7 and 3.4+, Jython and PyPy. It cooperates well with Python's ``help()`` system, custom importers (PEP302) and common command-line completion tools. Usage is very simple: you can require 'apipkg' as a dependency or you can copy paste the ~200 lines of code into your project. Tutorial example ------------------- Here is a simple ``mypkg`` package that specifies one namespace and exports two objects imported from different modules:: # mypkg/__init__.py import apipkg apipkg.initpkg(__name__, { 'path': { 'Class1': "_mypkg.somemodule:Class1", 'clsattr': "_mypkg.othermodule:Class2.attr", } } The package is initialized with a dictionary as namespace. You need to create a ``_mypkg`` package with a ``somemodule.py`` and ``othermodule.py`` containing the respective classes. The ``_mypkg`` is not special - it's a completely regular Python package. Namespace dictionaries contain ``name: value`` mappings where the value may be another namespace dictionary or a string specifying an import location. On accessing an namespace attribute an import will be performed:: >>> import mypkg >>> mypkg.path >>> mypkg.path.Class1 # '_mypkg.somemodule' gets imported now >>> mypkg.path.clsattr # '_mypkg.othermodule' gets imported now 4 # the value of _mypkg.othermodule.Class2.attr The ``mypkg.path`` namespace and its two entries are loaded when they are accessed. This means: * lazy loading - only what is actually needed is ever loaded * only the root "mypkg" ever needs to be imported to get access to the complete functionality * the underlying modules are also accessible, for example:: from mypkg.sub import Class1 Including apipkg in your package -------------------------------------- If you don't want to add an ``apipkg`` dependency to your package you can copy the `apipkg.py`_ file somewhere to your own package, for example ``_mypkg/apipkg.py`` in the above example. You then import the ``initpkg`` function from that new place and are good to go. .. _`small pure Python module`: .. _`apipkg.py`: https://github.com/pytest-dev/apipkg/blob/master/src/apipkg/__init__.py Feedback? ----------------------- If you have questions you are welcome to * join the **#pytest** channel on irc.libera.chat_ (using an IRC client, via webchat_, or via Matrix_). * create an issue on the bugtracker_ .. _irc.libera.chat: ircs://irc.libera.chat:6697/#pytest .. _webchat: https://web.libera.chat/#pytest .. _matrix: https://matrix.to/#/%23pytest:libera.chat .. _bugtracker: https://github.com/pytest-dev/apipkg/issues ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641425.0 apipkg-2.1.0/conftest.py0000644000175100001710000000054200000000000014525 0ustar00runnerdockerimport py import apipkg LOCAL_APIPKG = py.path.local(__file__).dirpath().join("src/apipkg/__init__.py") INSTALL_TYPE = "editable" if apipkg.__file__ == LOCAL_APIPKG else "full" def pytest_report_header(startdir): return "apipkg {install_type} install version={version}".format( install_type=INSTALL_TYPE, version=apipkg.__version__ ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1633641436.391064 apipkg-2.1.0/example/0000755000175100001710000000000000000000000013760 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1633641436.399064 apipkg-2.1.0/example/_mypkg/0000755000175100001710000000000000000000000015246 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641425.0 apipkg-2.1.0/example/_mypkg/__init__.py0000644000175100001710000000000000000000000017345 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641425.0 apipkg-2.1.0/example/_mypkg/othermodule.py0000644000175100001710000000003300000000000020143 0ustar00runnerdockerclass OtherClass: pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641425.0 apipkg-2.1.0/example/_mypkg/somemodule.py0000644000175100001710000000012400000000000017766 0ustar00runnerdockerfrom _mypkg.othermodule import OtherClass # NOQA: F401 class SomeClass: pass ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1633641436.399064 apipkg-2.1.0/example/mypkg/0000755000175100001710000000000000000000000015107 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641425.0 apipkg-2.1.0/example/mypkg/__init__.py0000644000175100001710000000033200000000000017216 0ustar00runnerdocker# mypkg/__init__.py import apipkg apipkg.initpkg( __name__, { "SomeClass": "_mypkg.somemodule:SomeClass", "sub": { "OtherClass": "_mypkg.somemodule:OtherClass", }, }, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641425.0 apipkg-2.1.0/example/mypkg/othermodule.py0000644000175100001710000000003300000000000020004 0ustar00runnerdockerclass OtherClass: pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641425.0 apipkg-2.1.0/example/mypkg/somemodule.py0000644000175100001710000000012500000000000017630 0ustar00runnerdockerfrom __mypkg.othermodule import OtherClass # NOQA: F401 class SomeClass: pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641425.0 apipkg-2.1.0/pyproject.toml0000644000175100001710000000021200000000000015234 0ustar00runnerdocker[build-system] requires = ["setuptools>=41.2.0", "wheel", "setuptools_scm>3"] [tool.setuptools_scm] write_to = "src/apipkg/version.py" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1633641436.403064 apipkg-2.1.0/setup.cfg0000644000175100001710000000254000000000000014147 0ustar00runnerdocker[metadata] name = apipkg description = apipkg: namespace control and lazy-import mechanism long_description = file: README.rst long_description_content_type = text/x-rst url = https://github.com/pytest-dev/apipkg author = holger krekel maintainer = Ronny Pfannschmidt maintainer_email = opensource@ronnypfannschmidt.de license = MIT license_file = LICENSE platforms = unix linux osx cygwin win32 classifiers = Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: MacOS :: MacOS X Operating System :: Microsoft :: Windows Operating System :: POSIX Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: Implementation :: CPython Topic :: Software Development :: Libraries [options] packages = find: python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* package_dir = =src setup_requires = setuptools>=30.3.0 setuptools_scm [options.packages.find] where = src [wheel] universal = true [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641425.0 apipkg-2.1.0/setup.py0000644000175100001710000000014200000000000014034 0ustar00runnerdockerfrom setuptools import setup setup( use_scm_version={"write_to": "src/apipkg/version.py"}, ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1633641436.391064 apipkg-2.1.0/src/0000755000175100001710000000000000000000000013114 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1633641436.399064 apipkg-2.1.0/src/apipkg/0000755000175100001710000000000000000000000014367 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641425.0 apipkg-2.1.0/src/apipkg/__init__.py0000644000175100001710000002455300000000000016511 0ustar00runnerdocker""" apipkg: control the exported namespace of a Python package. see https://pypi.python.org/pypi/apipkg (c) holger krekel, 2009 - MIT license """ import os import sys from types import ModuleType # Prior to Python 3.7 threading support was optional try: import threading except ImportError: threading = None else: import functools from .version import version as __version__ # NOQA:F401 _PY2 = sys.version_info[0] == 2 _PRESERVED_MODULE_ATTRS = { "__file__", "__version__", "__loader__", "__path__", "__package__", "__doc__", "__spec__", "__dict__", } def _py_abspath(path): """ special version of abspath that will leave paths from jython jars alone """ if path.startswith("__pyclasspath__"): return path else: return os.path.abspath(path) def distribution_version(name): """try to get the version of the named distribution, returs None on failure""" from pkg_resources import get_distribution, DistributionNotFound try: dist = get_distribution(name) except DistributionNotFound: pass else: return dist.version def initpkg(pkgname, exportdefs, attr=None, eager=False): """ initialize given package from the export definitions. """ attr = attr or {} mod = sys.modules.get(pkgname) if _PY2: mod = _initpkg_py2(mod, pkgname, exportdefs, attr=attr) else: mod = _initpkg_py3(mod, pkgname, exportdefs, attr=attr) # eagerload in bypthon to avoid their monkeypatching breaking packages if "bpython" in sys.modules or eager: for module in list(sys.modules.values()): if isinstance(module, ApiModule): module.__dict__ return mod def _initpkg_py2(mod, pkgname, exportdefs, attr=None): """Python 2 helper for initpkg. In Python 2 we can't update __class__ for an instance of types.Module, and imports are protected by the global import lock anyway, so it is safe for a module to replace itself during import. """ d = {} f = getattr(mod, "__file__", None) if f: f = _py_abspath(f) d["__file__"] = f if hasattr(mod, "__version__"): d["__version__"] = mod.__version__ if hasattr(mod, "__loader__"): d["__loader__"] = mod.__loader__ if hasattr(mod, "__path__"): d["__path__"] = [_py_abspath(p) for p in mod.__path__] if hasattr(mod, "__package__"): d["__package__"] = mod.__package__ if "__doc__" not in exportdefs and getattr(mod, "__doc__", None): d["__doc__"] = mod.__doc__ d["__spec__"] = getattr(mod, "__spec__", None) d.update(attr) if hasattr(mod, "__dict__"): mod.__dict__.update(d) mod = ApiModule(pkgname, exportdefs, implprefix=pkgname, attr=d) sys.modules[pkgname] = mod return mod def _initpkg_py3(mod, pkgname, exportdefs, attr=None): """Python 3 helper for initpkg. Python 3.3+ uses finer grained locking for imports, and checks sys.modules before acquiring the lock to avoid the overhead of the fine-grained locking. This introduces a race condition when a module is imported by multiple threads concurrently - some threads will see the initial module and some the replacement ApiModule. We avoid this by updating the existing module in-place. """ if mod is None: d = {"__file__": None, "__spec__": None} d.update(attr) mod = ApiModule(pkgname, exportdefs, implprefix=pkgname, attr=d) sys.modules[pkgname] = mod else: f = getattr(mod, "__file__", None) if f: f = _py_abspath(f) mod.__file__ = f if hasattr(mod, "__path__"): mod.__path__ = [_py_abspath(p) for p in mod.__path__] if "__doc__" in exportdefs and hasattr(mod, "__doc__"): del mod.__doc__ for name in dir(mod): if name not in _PRESERVED_MODULE_ATTRS: delattr(mod, name) # Updating class of existing module as per importlib.util.LazyLoader mod.__class__ = ApiModule mod.__init__(pkgname, exportdefs, implprefix=pkgname, attr=attr) return mod def importobj(modpath, attrname): """imports a module, then resolves the attrname on it""" module = __import__(modpath, None, None, ["__doc__"]) if not attrname: return module retval = module names = attrname.split(".") for x in names: retval = getattr(retval, x) return retval def _synchronized(wrapped_function): """Decorator to synchronise __getattr__ calls.""" if threading is None: return wrapped_function # Lock shared between all instances of ApiModule to avoid possible deadlocks lock = threading.RLock() @functools.wraps(wrapped_function) def synchronized_wrapper_function(*args, **kwargs): with lock: return wrapped_function(*args, **kwargs) return synchronized_wrapper_function class ApiModule(ModuleType): """the magical lazy-loading module standing""" def __docget(self): try: return self.__doc except AttributeError: if "__doc__" in self.__map__: return self.__makeattr("__doc__") def __docset(self, value): self.__doc = value __doc__ = property(__docget, __docset) def __init__(self, name, importspec, implprefix=None, attr=None): self.__name__ = name self.__all__ = [x for x in importspec if x != "__onfirstaccess__"] self.__map__ = {} self.__implprefix__ = implprefix or name if attr: for name, val in attr.items(): setattr(self, name, val) for name, importspec in importspec.items(): if isinstance(importspec, dict): subname = "{}.{}".format(self.__name__, name) apimod = ApiModule(subname, importspec, implprefix) sys.modules[subname] = apimod setattr(self, name, apimod) else: parts = importspec.split(":") modpath = parts.pop(0) attrname = parts and parts[0] or "" if modpath[0] == ".": modpath = implprefix + modpath if not attrname: subname = "{}.{}".format(self.__name__, name) apimod = AliasModule(subname, modpath) sys.modules[subname] = apimod if "." not in name: setattr(self, name, apimod) else: self.__map__[name] = (modpath, attrname) def __repr__(self): repr_list = [] if hasattr(self, "__version__"): repr_list.append("version=" + repr(self.__version__)) if hasattr(self, "__file__"): repr_list.append("from " + repr(self.__file__)) if repr_list: return "".format(self.__name__, " ".join(repr_list)) return "".format(self.__name__) @_synchronized def __makeattr(self, name, isgetattr=False): """lazily compute value for name or raise AttributeError if unknown.""" target = None if "__onfirstaccess__" in self.__map__: target = self.__map__.pop("__onfirstaccess__") importobj(*target)() try: modpath, attrname = self.__map__[name] except KeyError: # __getattr__ is called when the attribute does not exist, but it may have # been set by the onfirstaccess call above. Infinite recursion is not # possible as __onfirstaccess__ is removed before the call (unless the call # adds __onfirstaccess__ to __map__ explicitly, which is not our problem) if target is not None and name != "__onfirstaccess__": return getattr(self, name) # Attribute may also have been set during a concurrent call to __getattr__ # which executed after this call was already waiting on the lock. Check # for a recently set attribute while avoiding infinite recursion: # * Don't call __getattribute__ if __makeattr was called from a data # descriptor such as the __doc__ or __dict__ properties, since data # descriptors are called as part of object.__getattribute__ # * Only call __getattribute__ if there is a possibility something has set # the attribute we're looking for since __getattr__ was called if threading is not None and isgetattr: return super(ApiModule, self).__getattribute__(name) raise AttributeError(name) else: result = importobj(modpath, attrname) setattr(self, name, result) try: del self.__map__[name] except KeyError: pass # in a recursive-import situation a double-del can happen return result def __getattr__(self, name): return self.__makeattr(name, isgetattr=True) @property def __dict__(self): # force all the content of the module # to be loaded when __dict__ is read dictdescr = ModuleType.__dict__["__dict__"] dict = dictdescr.__get__(self) if dict is not None: hasattr(self, "some") for name in self.__all__: try: self.__makeattr(name) except AttributeError: pass return dict def AliasModule(modname, modpath, attrname=None): mod = [] def getmod(): if not mod: x = importobj(modpath, None) if attrname is not None: x = getattr(x, attrname) mod.append(x) return mod[0] x = modpath + ("." + attrname if attrname else "") repr_result = "".format(modname, x) class AliasModule(ModuleType): def __repr__(self): return repr_result def __getattribute__(self, name): try: return getattr(getmod(), name) except ImportError: if modpath == "pytest" and attrname is None: # hack for pylibs py.test return None else: raise def __setattr__(self, name, value): setattr(getmod(), name, value) def __delattr__(self, name): delattr(getmod(), name) return AliasModule(str(modname)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641436.0 apipkg-2.1.0/src/apipkg/version.py0000644000175100001710000000021600000000000016425 0ustar00runnerdocker# coding: utf-8 # file generated by setuptools_scm # don't change, don't track in version control version = '2.1.0' version_tuple = (2, 1, 0) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1633641436.399064 apipkg-2.1.0/src/apipkg.egg-info/0000755000175100001710000000000000000000000016061 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641436.0 apipkg-2.1.0/src/apipkg.egg-info/PKG-INFO0000644000175100001710000001030400000000000017154 0ustar00runnerdockerMetadata-Version: 2.1 Name: apipkg Version: 2.1.0 Summary: apipkg: namespace control and lazy-import mechanism Home-page: https://github.com/pytest-dev/apipkg Author: holger krekel Maintainer: Ronny Pfannschmidt Maintainer-email: opensource@ronnypfannschmidt.de License: MIT Platform: unix Platform: linux Platform: osx Platform: cygwin Platform: win32 Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Software Development :: Libraries Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7 Description-Content-Type: text/x-rst License-File: LICENSE Welcome to apipkg ! ------------------- With apipkg you can control the exported namespace of a Python package and greatly reduce the number of imports for your users. It is a `small pure Python module`_ that works on CPython 2.7 and 3.4+, Jython and PyPy. It cooperates well with Python's ``help()`` system, custom importers (PEP302) and common command-line completion tools. Usage is very simple: you can require 'apipkg' as a dependency or you can copy paste the ~200 lines of code into your project. Tutorial example ------------------- Here is a simple ``mypkg`` package that specifies one namespace and exports two objects imported from different modules:: # mypkg/__init__.py import apipkg apipkg.initpkg(__name__, { 'path': { 'Class1': "_mypkg.somemodule:Class1", 'clsattr': "_mypkg.othermodule:Class2.attr", } } The package is initialized with a dictionary as namespace. You need to create a ``_mypkg`` package with a ``somemodule.py`` and ``othermodule.py`` containing the respective classes. The ``_mypkg`` is not special - it's a completely regular Python package. Namespace dictionaries contain ``name: value`` mappings where the value may be another namespace dictionary or a string specifying an import location. On accessing an namespace attribute an import will be performed:: >>> import mypkg >>> mypkg.path >>> mypkg.path.Class1 # '_mypkg.somemodule' gets imported now >>> mypkg.path.clsattr # '_mypkg.othermodule' gets imported now 4 # the value of _mypkg.othermodule.Class2.attr The ``mypkg.path`` namespace and its two entries are loaded when they are accessed. This means: * lazy loading - only what is actually needed is ever loaded * only the root "mypkg" ever needs to be imported to get access to the complete functionality * the underlying modules are also accessible, for example:: from mypkg.sub import Class1 Including apipkg in your package -------------------------------------- If you don't want to add an ``apipkg`` dependency to your package you can copy the `apipkg.py`_ file somewhere to your own package, for example ``_mypkg/apipkg.py`` in the above example. You then import the ``initpkg`` function from that new place and are good to go. .. _`small pure Python module`: .. _`apipkg.py`: https://github.com/pytest-dev/apipkg/blob/master/src/apipkg/__init__.py Feedback? ----------------------- If you have questions you are welcome to * join the **#pytest** channel on irc.libera.chat_ (using an IRC client, via webchat_, or via Matrix_). * create an issue on the bugtracker_ .. _irc.libera.chat: ircs://irc.libera.chat:6697/#pytest .. _webchat: https://web.libera.chat/#pytest .. _matrix: https://matrix.to/#/%23pytest:libera.chat .. _bugtracker: https://github.com/pytest-dev/apipkg/issues ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641436.0 apipkg-2.1.0/src/apipkg.egg-info/SOURCES.txt0000644000175100001710000000105700000000000017750 0ustar00runnerdocker.gitignore .pre-commit-config.yaml CHANGELOG LICENSE MANIFEST.in README.rst conftest.py pyproject.toml setup.cfg setup.py test_apipkg.py tox.ini .github/workflows/pre-commit.yml .github/workflows/pythonapp.yml example/_mypkg/__init__.py example/_mypkg/othermodule.py example/_mypkg/somemodule.py example/mypkg/__init__.py example/mypkg/othermodule.py example/mypkg/somemodule.py src/apipkg/__init__.py src/apipkg/version.py src/apipkg.egg-info/PKG-INFO src/apipkg.egg-info/SOURCES.txt src/apipkg.egg-info/dependency_links.txt src/apipkg.egg-info/top_level.txt././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641436.0 apipkg-2.1.0/src/apipkg.egg-info/dependency_links.txt0000644000175100001710000000000100000000000022127 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641436.0 apipkg-2.1.0/src/apipkg.egg-info/top_level.txt0000644000175100001710000000000700000000000020610 0ustar00runnerdockerapipkg ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641425.0 apipkg-2.1.0/test_apipkg.py0000644000175100001710000005731200000000000015221 0ustar00runnerdockerimport os.path import subprocess import sys import textwrap import types try: import threading except ImportError: pass import pytest import apipkg PY2 = sys.version_info[0] == 2 PY3 = sys.version_info[0] == 3 # # test support for importing modules # ModuleType = types.ModuleType class TestRealModule: @pytest.fixture(scope="class", autouse=True) def make_modules(cls, tmpdir_factory): cls.tmpdir = tmpdir_factory.mktemp("test_apipkg") sys.path = [str(cls.tmpdir)] + sys.path pkgdir = cls.tmpdir.ensure("realtest", dir=1) tfile = pkgdir.join("__init__.py") tfile.write( textwrap.dedent( """ import apipkg apipkg.initpkg(__name__, { 'x': { 'module': { '__doc__': '_xyz.testmodule:__doc__', 'mytest0': '_xyz.testmodule:mytest0', 'mytest1': '_xyz.testmodule:mytest1', 'MyTest': '_xyz.testmodule:MyTest', } } } ) """ ) ) ipkgdir = cls.tmpdir.ensure("_xyz", dir=1) tfile = ipkgdir.join("testmodule.py") ipkgdir.ensure("__init__.py") tfile.write( textwrap.dedent( """ 'test module' from _xyz.othermodule import MyTest #__all__ = ['mytest0', 'mytest1', 'MyTest'] def mytest0(): pass def mytest1(): pass """ ) ) ipkgdir.join("othermodule.py").write("class MyTest: pass") def setup_method(self, *args): # Unload the test modules before each test. module_names = "realtest", "realtest.x", "realtest.x.module" for modname in module_names: sys.modules.pop(modname, None) def test_realmodule(self): import realtest.x assert "realtest.x.module" in sys.modules assert getattr(realtest.x.module, "mytest0") def test_realmodule_repr(self): import realtest.x assert "" == repr(realtest.x) def test_realmodule_from(self): from realtest.x import module assert getattr(module, "mytest1") def test_realmodule__all__(self): import realtest.x.module assert realtest.x.__all__ == ["module"] assert len(realtest.x.module.__all__) == 4 def test_realmodule_dict_import(self): "Test verifying that accessing the __dict__ invokes the import" import realtest.x.module moddict = realtest.x.module.__dict__ assert "mytest0" in moddict assert "mytest1" in moddict assert "MyTest" in moddict def test_realmodule___doc__(self): """test whether the __doc__ attribute is set properly from initpkg""" import realtest.x.module print(realtest.x.module.__map__) assert realtest.x.module.__doc__ == "test module" class TestScenarios: def test_relative_import(self, monkeypatch, tmpdir): pkgdir = tmpdir.mkdir("mymodule") pkgdir.join("__init__.py").write( textwrap.dedent( """ import apipkg apipkg.initpkg(__name__, exportdefs={ '__doc__': '.submod:maindoc', 'x': '.submod:x', 'y': { 'z': '.submod:x' }, }) """ ) ) pkgdir.join("submod.py").write("x=3\nmaindoc='hello'") monkeypatch.syspath_prepend(tmpdir) import mymodule assert isinstance(mymodule, apipkg.ApiModule) assert mymodule.x == 3 assert mymodule.__doc__ == "hello" assert mymodule.y.z == 3 def test_recursive_import(self, monkeypatch, tmpdir): pkgdir = tmpdir.mkdir("recmodule") pkgdir.join("__init__.py").write( textwrap.dedent( """ import apipkg apipkg.initpkg(__name__, exportdefs={ 'some': '.submod:someclass', }) """ ) ) pkgdir.join("submod.py").write( textwrap.dedent( """ import recmodule class someclass: pass print(recmodule.__dict__) """ ) ) monkeypatch.syspath_prepend(tmpdir) import recmodule assert isinstance(recmodule, apipkg.ApiModule) assert recmodule.some.__name__ == "someclass" def test_module_alias_import(self, monkeypatch, tmpdir): pkgdir = tmpdir.mkdir("aliasimport") pkgdir.join("__init__.py").write( textwrap.dedent( """ import apipkg apipkg.initpkg(__name__, exportdefs={ 'some': 'os.path', }) """ ) ) monkeypatch.syspath_prepend(tmpdir) import aliasimport for k, v in os.path.__dict__.items(): assert getattr(aliasimport.some, k) == v def test_from_module_alias_import(self, monkeypatch, tmpdir): pkgdir = tmpdir.mkdir("fromaliasimport") pkgdir.join("__init__.py").write( textwrap.dedent( """ import apipkg apipkg.initpkg(__name__, exportdefs={ 'some': 'os.path', }) """ ) ) monkeypatch.syspath_prepend(tmpdir) from fromaliasimport.some import join assert join is os.path.join def xtest_nested_absolute_imports(): apipkg.ApiModule( "email", { "message2": { "Message": "email.message:Message", }, }, ) # nesting is supposed to put nested items into sys.modules assert "email.message2" in sys.modules # alternate ideas for specifying package + preliminary code # def test_parsenamespace(): spec = """ path.local __.path.local::LocalPath path.svnwc __.path.svnwc::WCCommandPath test.raises __.test.outcome::raises """ d = parsenamespace(spec) print(d) assert d == { "test": {"raises": "__.test.outcome::raises"}, "path": { "svnwc": "__.path.svnwc::WCCommandPath", "local": "__.path.local::LocalPath", }, } def xtest_parsenamespace_errors(): pytest.raises( ValueError, """ parsenamespace('path.local xyz') """, ) pytest.raises( ValueError, """ parsenamespace('x y z') """, ) def parsenamespace(spec): ns = {} for line in spec.split("\n"): line = line.strip() if not line or line[0] == "#": continue parts = [x.strip() for x in line.split()] if len(parts) != 2: raise ValueError("Wrong format: {!r}".format(line)) apiname, spec = parts if not spec.startswith("__"): raise ValueError("{!r} does not start with __".format(spec)) apinames = apiname.split(".") cur = ns for name in apinames[:-1]: cur.setdefault(name, {}) cur = cur[name] cur[apinames[-1]] = spec return ns def test_initpkg_updates_sysmodules(monkeypatch): mod = ModuleType("hello") monkeypatch.setitem(sys.modules, "hello", mod) apipkg.initpkg("hello", {"x": "os.path:abspath"}) newmod = sys.modules["hello"] assert (PY2 and newmod != mod) or (PY3 and newmod is mod) assert newmod.x == os.path.abspath def test_initpkg_transfers_attrs(monkeypatch): mod = ModuleType("hello") mod.__version__ = 10 mod.__file__ = "hello.py" mod.__loader__ = "loader" mod.__package__ = "package" mod.__doc__ = "this is the documentation" mod.goodbye = "goodbye" monkeypatch.setitem(sys.modules, "hello", mod) apipkg.initpkg("hello", {}) newmod = sys.modules["hello"] assert (PY2 and newmod != mod) or (PY3 and newmod is mod) assert newmod.__file__ == os.path.abspath(mod.__file__) assert newmod.__version__ == mod.__version__ assert newmod.__loader__ == mod.__loader__ assert newmod.__package__ == mod.__package__ assert newmod.__doc__ == mod.__doc__ assert not hasattr(newmod, "goodbye") def test_initpkg_nodoc(monkeypatch): mod = ModuleType("hello") mod.__file__ = "hello.py" monkeypatch.setitem(sys.modules, "hello", mod) apipkg.initpkg("hello", {}) newmod = sys.modules["hello"] assert not newmod.__doc__ def test_initpkg_overwrite_doc(monkeypatch): hello = ModuleType("hello") hello.__doc__ = "this is the documentation" monkeypatch.setitem(sys.modules, "hello", hello) apipkg.initpkg("hello", {"__doc__": "sys:__doc__"}) newhello = sys.modules["hello"] assert (PY2 and newhello != hello) or (PY3 and newhello is hello) assert newhello.__doc__ == sys.__doc__ def test_initpkg_not_transfers_not_existing_attrs(monkeypatch): mod = ModuleType("hello") mod.__file__ = "hello.py" assert not hasattr(mod, "__path__") assert not hasattr(mod, "__package__") or mod.__package__ is None monkeypatch.setitem(sys.modules, "hello", mod) apipkg.initpkg("hello", {}) newmod = sys.modules["hello"] assert (PY2 and newmod != mod) or (PY3 and newmod is mod) assert newmod.__file__ == os.path.abspath(mod.__file__) assert not hasattr(newmod, "__path__") assert not hasattr(newmod, "__package__") or mod.__package__ is None def test_initpkg_not_changing_jython_paths(monkeypatch): mod = ModuleType("hello") mod.__file__ = "__pyclasspath__/test.py" mod.__path__ = ["__pyclasspath__/fun", "ichange"] monkeypatch.setitem(sys.modules, "hello", mod) apipkg.initpkg("hello", {}) newmod = sys.modules["hello"] assert (PY2 and newmod != mod) or (PY3 and newmod is mod) assert newmod.__file__.startswith("__pyclasspath__") unchanged, changed = newmod.__path__ assert changed != "ichange" assert unchanged.startswith("__pyclasspath__") def test_initpkg_defaults(monkeypatch): mod = ModuleType("hello") monkeypatch.setitem(sys.modules, "hello", mod) apipkg.initpkg("hello", {}) newmod = sys.modules["hello"] assert newmod.__file__ is None assert not hasattr(newmod, "__version__") def test_name_attribute(): api = apipkg.ApiModule( "name_test", { "subpkg": {}, }, ) assert api.__name__ == "name_test" assert api.subpkg.__name__ == "name_test.subpkg" def test_error_loading_one_element(monkeypatch, tmpdir): pkgdir = tmpdir.mkdir("errorloading1") pkgdir.join("__init__.py").write( textwrap.dedent( """ import apipkg apipkg.initpkg(__name__, exportdefs={ 'x': '.notexists:x', 'y': '.submod:y' }, ) """ ) ) pkgdir.join("submod.py").write("y=0") monkeypatch.syspath_prepend(tmpdir) import errorloading1 assert isinstance(errorloading1, apipkg.ApiModule) assert errorloading1.y == 0 with pytest.raises(ImportError): errorloading1.x with pytest.raises(ImportError): errorloading1.x def test_onfirstaccess(tmpdir, monkeypatch): pkgdir = tmpdir.mkdir("firstaccess") pkgdir.join("__init__.py").write( textwrap.dedent( """ import apipkg apipkg.initpkg(__name__, exportdefs={ '__onfirstaccess__': '.submod:init', 'l': '.submod:l', }, ) """ ) ) pkgdir.join("submod.py").write( textwrap.dedent( """ l = [] def init(): l.append(1) """ ) ) monkeypatch.syspath_prepend(tmpdir) import firstaccess assert isinstance(firstaccess, apipkg.ApiModule) assert len(firstaccess.l) == 1 assert len(firstaccess.l) == 1 assert "__onfirstaccess__" not in firstaccess.__all__ @pytest.mark.parametrize("mode", ["attr", "dict", "onfirst"]) def test_onfirstaccess_setsnewattr(tmpdir, monkeypatch, mode): pkgname = "mode_" + mode pkgdir = tmpdir.mkdir(pkgname) pkgdir.join("__init__.py").write( textwrap.dedent( """ import apipkg apipkg.initpkg(__name__, exportdefs={ '__onfirstaccess__': '.submod:init', }, ) """ ) ) pkgdir.join("submod.py").write( textwrap.dedent( """ def init(): import %s as pkg pkg.newattr = 42 """ % pkgname ) ) monkeypatch.syspath_prepend(tmpdir) mod = __import__(pkgname) assert isinstance(mod, apipkg.ApiModule) if mode == "attr": assert mod.newattr == 42 elif mode == "dict": print(list(mod.__dict__.keys())) assert "newattr" in mod.__dict__ elif mode == "onfirst": assert not hasattr(mod, "__onfirstaccess__") assert not hasattr(mod, "__onfirstaccess__") assert "__onfirstaccess__" not in vars(mod) @pytest.mark.skipif("threading" not in sys.modules, reason="requires thread support") def test_onfirstaccess_race(tmpdir, monkeypatch): pkgdir = tmpdir.mkdir("firstaccessrace") pkgdir.join("__init__.py").write( textwrap.dedent( """ import apipkg apipkg.initpkg(__name__, exportdefs={ '__onfirstaccess__': '.submod:init', 'l': '.submod:l', }, ) """ ) ) pkgdir.join("submod.py").write( textwrap.dedent( """ import time l = [] def init(): time.sleep(0.1) l.append(1) """ ) ) monkeypatch.syspath_prepend(tmpdir) import firstaccessrace assert isinstance(firstaccessrace, apipkg.ApiModule) class TestThread(threading.Thread): def __init__(self, event_start): super(TestThread, self).__init__() self.event_start = event_start self.lenl = None def run(self): self.event_start.wait() self.lenl = len(firstaccessrace.l) event_start = threading.Event() threads = [TestThread(event_start) for _ in range(8)] for thread in threads: thread.start() event_start.set() for thread in threads: thread.join() assert len(firstaccessrace.l) == 1 for thread in threads: assert thread.lenl == 1 assert "__onfirstaccess__" not in firstaccessrace.__all__ @pytest.mark.skipif("threading" not in sys.modules, reason="requires thread support") def test_attribute_race(tmpdir, monkeypatch): pkgdir = tmpdir.mkdir("attributerace") pkgdir.join("__init__.py").write( textwrap.dedent( """ import apipkg apipkg.initpkg(__name__, exportdefs={ 'attr': '.submod:attr', }, ) """ ) ) pkgdir.join("submod.py").write( textwrap.dedent( """ import time time.sleep(0.1) attr = 42 """ ) ) monkeypatch.syspath_prepend(tmpdir) import attributerace assert isinstance(attributerace, apipkg.ApiModule) class TestThread(threading.Thread): def __init__(self, event_start): super(TestThread, self).__init__() self.event_start = event_start self.attr = None def run(self): self.event_start.wait() self.attr = attributerace.attr event_start = threading.Event() threads = [TestThread(event_start) for _ in range(8)] for thread in threads: thread.start() event_start.set() for thread in threads: thread.join() assert attributerace.attr == 42 for thread in threads: assert thread.attr == 42 @pytest.mark.skipif("threading" not in sys.modules, reason="requires thread support") def test_import_race(tmpdir, monkeypatch): pkgdir = tmpdir.mkdir("importrace") pkgdir.join("__init__.py").write( textwrap.dedent( """ import time time.sleep(0.1) import apipkg apipkg.initpkg(__name__, exportdefs={ 'attr': '.submod:attr', }, ) """ ) ) pkgdir.join("submod.py").write( textwrap.dedent( """ attr = 43 """ ) ) monkeypatch.syspath_prepend(tmpdir) class TestThread(threading.Thread): def __init__(self): super(TestThread, self).__init__() self.importrace = None def run(self): import importrace self.importrace = importrace threads = [TestThread() for _ in range(8)] for thread in threads: thread.start() for thread in threads: thread.join() for thread in threads: assert isinstance(thread.importrace, apipkg.ApiModule) assert thread.importrace.attr == 43 import importrace assert isinstance(importrace, apipkg.ApiModule) assert importrace.attr == 43 for thread in threads: assert thread.importrace is importrace def test_bpython_getattr_override(tmpdir, monkeypatch): def patchgetattr(self, name): raise AttributeError(name) monkeypatch.setattr(apipkg.ApiModule, "__getattr__", patchgetattr) api = apipkg.ApiModule( "bpy", { "abspath": "os.path:abspath", }, ) d = api.__dict__ assert "abspath" in d def test_chdir_with_relative_imports_support_lazy_loading(tmpdir, monkeypatch): monkeypatch.syspath_prepend(os.path.abspath("src")) pkg = tmpdir.mkdir("pkg") tmpdir.mkdir("messy") pkg.join("__init__.py").write( textwrap.dedent( """ import apipkg apipkg.initpkg(__name__, { 'test': '.sub:test', }) """ ) ) pkg.join("sub.py").write("def test(): pass") tmpdir.join("main.py").write( textwrap.dedent( """ from __future__ import print_function import os import sys sys.path.insert(0, '') import pkg print(pkg.__path__, file=sys.stderr) print(pkg.__file__, file=sys.stderr) print(pkg, file=sys.stderr) os.chdir('messy') pkg.test() assert os.path.isabs(pkg.sub.__file__), pkg.sub.__file__ """ ) ) res = subprocess.call( [sys.executable, "main.py"], cwd=str(tmpdir), ) assert res == 0 def test_dotted_name_lookup(tmpdir, monkeypatch): pkgdir = tmpdir.mkdir("dotted_name_lookup") pkgdir.join("__init__.py").write( textwrap.dedent( """ import apipkg apipkg.initpkg(__name__, dict(abs='os:path.abspath')) """ ) ) monkeypatch.syspath_prepend(tmpdir) import dotted_name_lookup assert dotted_name_lookup.abs == os.path.abspath def test_extra_attributes(tmpdir, monkeypatch): pkgdir = tmpdir.mkdir("extra_attributes") pkgdir.join("__init__.py").write( textwrap.dedent( """ import apipkg apipkg.initpkg(__name__, dict(abs='os:path.abspath'), dict(foo='bar')) """ ) ) monkeypatch.syspath_prepend(tmpdir) import extra_attributes assert extra_attributes.foo == "bar" def test_aliasmodule_aliases_an_attribute(): am = apipkg.AliasModule("mymod", "pprint", "PrettyPrinter") r = repr(am) assert "" == r assert am.format assert not hasattr(am, "lqkje") def test_aliasmodule_aliases_unimportable_fails(): am = apipkg.AliasModule("mymod", "qlwkejqlwe", "main") r = repr(am) assert "" == r # this would pass starting with apipkg 1.3 to work around a pytest bug with pytest.raises(ImportError): am.qwe is None def test_aliasmodule_pytest_autoreturn_none_for_hack(monkeypatch): def error(*k): raise ImportError(k) monkeypatch.setattr(apipkg, "importobj", error) # apipkg 1.3 added this hack am = apipkg.AliasModule("mymod", "pytest") r = repr(am) assert "" == r assert am.test is None def test_aliasmodule_unicode(): am = apipkg.AliasModule(u"mymod", "pprint") assert am def test_aliasmodule_repr(): am = apipkg.AliasModule("mymod", "sys") r = repr(am) assert "" == r am.version assert repr(am) == r def test_aliasmodule_proxy_methods(tmpdir, monkeypatch): pkgdir = tmpdir pkgdir.join("aliasmodule_proxy.py").write( textwrap.dedent( """ def doit(): return 42 """ ) ) pkgdir.join("my_aliasmodule_proxy.py").write( textwrap.dedent( """ import apipkg apipkg.initpkg(__name__, dict(proxy='aliasmodule_proxy')) def doit(): return 42 """ ) ) monkeypatch.syspath_prepend(tmpdir) import aliasmodule_proxy as orig from my_aliasmodule_proxy import proxy doit = proxy.doit assert doit is orig.doit del proxy.doit with pytest.raises(AttributeError): orig.doit proxy.doit = doit assert orig.doit is doit def test_aliasmodule_nested_import_with_from(tmpdir, monkeypatch): import os pkgdir = tmpdir.mkdir("api1") pkgdir.ensure("__init__.py").write( textwrap.dedent( """ import apipkg apipkg.initpkg(__name__, { 'os2': 'api2', 'os2.path': 'api2.path2', }) """ ) ) tmpdir.join("api2.py").write( textwrap.dedent( """ import os, sys from os import path sys.modules['api2.path2'] = path x = 3 """ ) ) monkeypatch.syspath_prepend(tmpdir) from api1 import os2 from api1.os2.path import abspath assert abspath == os.path.abspath # check that api1.os2 mirrors os.* assert os2.x == 3 import api1 assert "os2.path" not in api1.__dict__ def test_initpkg_without_old_module(): apipkg.initpkg("initpkg_without_old_module", dict(modules="sys:modules")) from initpkg_without_old_module import modules assert modules is sys.modules def test_get_distribution_version(): assert apipkg.distribution_version("setuptools") is not None assert apipkg.distribution_version("email") is None assert apipkg.distribution_version("py") is not None def test_eagerload_on_bython(monkeypatch): monkeypatch.delitem(sys.modules, "bpython", raising=False) apipkg.initpkg("apipkg.testmodule.example.lazy", {"test": "apipkg.does_not_exist"}) monkeypatch.setitem(sys.modules, "bpython", True) with pytest.raises(ImportError): apipkg.initpkg( "apipkg.testmodule.example.lazy", {"test": "apipkg.does_not_exist"} ) @pytest.fixture def find_spec(): try: from importlib.util import find_spec except ImportError: pytest.xfail("no importlib") return find_spec def test_importlib_find_spec_fake_module(find_spec): mod = apipkg.initpkg("apipkg.testmodule.example.missing", {}) with pytest.raises(ValueError, match=mod.__name__ + r"\.__spec__ is None"): find_spec(mod.__name__) def test_importlib_find_spec_aliasmodule(find_spec): am = apipkg.AliasModule("apipkg.testmodule.example.email_spec", "email") spec = find_spec(am.__name__) assert spec is am.__spec__ def test_importlib_find_spec_initpkg(find_spec, tmpdir, monkeypatch): modname = "apipkg_test_example_initpkg_findspec" pkgdir = tmpdir.mkdir("apipkg_test_example_initpkg_findspec") pkgdir.ensure("__init__.py").write( textwrap.dedent( """ import apipkg apipkg.initpkg(__name__, { 'email': 'email', }) """ ) ) monkeypatch.syspath_prepend(tmpdir) find_spec(modname) find_spec(modname + ".email") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1633641425.0 apipkg-2.1.0/tox.ini0000644000175100001710000000035500000000000013643 0ustar00runnerdocker[tox] envlist=py27,py34,py35,py36,py37,py38,py39 [testenv] deps=pytest commands=pytest [] [testenv:jython] deps=pytest commands=pytest-jython [] [flake8] exclude=.tox/,.env/,dist/,build/,example/ max_complexity=11 max-line-length=88