diff --git a/.gitignore b/.gitignore index 6ceeb570..a4fc2069 100644 --- a/.gitignore +++ b/.gitignore @@ -226,7 +226,7 @@ cpyamf/codec.pyd cpyamf/util.pyd dist file_model -pyamf/_version.py +src/pyamf/_version.py # coverage .coverage .noseids diff --git a/.travis.yml b/.travis.yml index b6634f96..32622069 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,8 @@ language: python python: - - "3.5" - - "3.6" - - "3.7" - - "3.8" + - "3.12" + - "3.13" + - "3.14" sudo: false cache: @@ -44,14 +43,13 @@ matrix: # env: GAESDK_VERSION=1.9.30 install: - - pip install -r requirements.txt - pip install coverage coveralls - - pip install -e . + - pip install -e .[test] - ./install_optional_dependencies.sh - ./maybe_install_cython.sh script: - - coverage run --source=pyamf setup.py test + - coverage run --source=pyamf pytest ./pyamf after_success: - coveralls diff --git a/MANIFEST.in b/MANIFEST.in index 1de8b8e6..366b7ea2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,4 +8,4 @@ include *.md global-exclude *.swf global-exclude *.pyc include cpyamf/*.pyx -include cpyamf/*.pxd \ No newline at end of file +include cpyamf/*.pxd diff --git a/README.md b/README.md index 6f6032de..bd5eae88 100644 --- a/README.md +++ b/README.md @@ -18,42 +18,57 @@ If you want to make it fast, please send PR. This was tested on Ubuntu 16.04.2 and macOS 10.12.4 To install, you can use pip3 on your environment. + ``` -pip3 install Py3AMF +pip install py3amf ``` Or, you can use setup.py to develop. ``` git clone git@github.com:StdCarrot/Py3AMF.git cd Py3AMF -# python3 setup.py test -python3 setup.py install +# pip install -e .[test] && pytest ./pyamf +pip install -e . +``` + +The binary extension is compiled by default. To disable it, use the `PYAMF_DISABLE_EXT` env +variable. + +```shell +PYAMF_DISABLE_EXT=1 pip install py3amf +# or +PYAMF_DISABLE_EXT=1 pip install -e . ``` ### Simple example Everything is same with PyAMF, but you have to concern str and bytes types. + ```python import pyamf -from pyamf import remoting +from src.pyamf import remoting from pyamf.flex import messaging import uuid import requests -msg = messaging.RemotingMessage(operation='retrieveUser', - destination='so.stdc.flexact.common.User', - messageId=str(uuid.uuid4()).upper(), - body=['user_id']) +msg = messaging.RemotingMessage( + operation='retrieveUser', + destination='so.stdc.flexact.common.User', + messageId=str(uuid.uuid4()).upper(), + body=['user_id'] +) req = remoting.Request(target='UserService', body=[msg]) -ev = remoting.Envelope(pyamf.AMF3) +ev = remoting.Envelope(pyamf.AMF3) ev['/0'] = req # Encode request bin_msg = remoting.encode(ev) # Send request; You can use other channels like RTMP -resp = requests.post('http://example.com/amf', - data=bin_msg.getvalue(), - headers={'Content-Type': 'application/x-amf'}) +resp = requests.post( + 'http://example.com/amf', + data=bin_msg.getvalue(), + headers={'Content-Type': 'application/x-amf'} +) # Decode response resp_msg = remoting.decode(resp.content) diff --git a/README.rst b/README.rst index 333a1611..9bd0693b 100644 --- a/README.rst +++ b/README.rst @@ -33,7 +33,7 @@ To install, you can use pip3 on your environment. :: - pip3 install Py3AMF + pip3 install py3amf Or, you can use setup.py to develop. @@ -41,8 +41,17 @@ Or, you can use setup.py to develop. git clone git@github.com:StdCarrot/Py3AMF.git cd Py3AMF - # python3 setup.py test - python3 setup.py install + # pip install -e .[test] && pytest ./pyamf + pip install -e . + +The binary extension is compiled by default. To disable it, use the `PYAMF_DISABLE_EXT` env +variable. + +:: + + PYAMF_DISABLE_EXT=1 pip install py3amf + # or + PYAMF_DISABLE_EXT=1 pip install -e . Simple example ~~~~~~~~~~~~~~ diff --git a/doc/tutorials/examples/apache/mod_python.py b/doc/tutorials/examples/apache/mod_python.py index 14d70532..e8815137 100644 --- a/doc/tutorials/examples/apache/mod_python.py +++ b/doc/tutorials/examples/apache/mod_python.py @@ -1,4 +1,4 @@ -from pyamf.remoting.gateway.wsgi import WSGIGateway +from src.pyamf.remoting import WSGIGateway import logging diff --git a/doc/tutorials/examples/apache/mod_wsgi.py b/doc/tutorials/examples/apache/mod_wsgi.py index f798c2d6..ec048cf8 100644 --- a/doc/tutorials/examples/apache/mod_wsgi.py +++ b/doc/tutorials/examples/apache/mod_wsgi.py @@ -1,4 +1,4 @@ -from pyamf.remoting.gateway.wsgi import WSGIGateway +from src.pyamf.remoting import WSGIGateway def echo(data): return data diff --git a/doc/tutorials/examples/gateways/appengine/client.py b/doc/tutorials/examples/gateways/appengine/client.py index f20062a7..5d4255e2 100644 --- a/doc/tutorials/examples/gateways/appengine/client.py +++ b/doc/tutorials/examples/gateways/appengine/client.py @@ -1,8 +1,8 @@ import logging -from pyamf.remoting.client import RemotingService +from src.pyamf.remoting import RemotingService + - logging.basicConfig( level=logging.DEBUG, format='%(asctime)s %(levelname)-5.5s [%(name)s] %(message)s' diff --git a/doc/tutorials/examples/gateways/cherrypy/client.py b/doc/tutorials/examples/gateways/cherrypy/client.py index 3a574cfc..09b25219 100644 --- a/doc/tutorials/examples/gateways/cherrypy/client.py +++ b/doc/tutorials/examples/gateways/cherrypy/client.py @@ -1,8 +1,8 @@ import logging -from pyamf.remoting.client import RemotingService +from src.pyamf.remoting import RemotingService + - logging.basicConfig( level=logging.DEBUG, format='%(asctime)s %(levelname)-5.5s [%(name)s] %(message)s' @@ -13,4 +13,4 @@ gw = RemotingService(path, logger=logging) service = gw.getService('myservice') -print service.echo('Hello World!') \ No newline at end of file +print service.echo('Hello World!') diff --git a/doc/tutorials/examples/gateways/turbogears/mygateway.py b/doc/tutorials/examples/gateways/turbogears/mygateway.py index 01a6fc3d..c84ad60b 100644 --- a/doc/tutorials/examples/gateways/turbogears/mygateway.py +++ b/doc/tutorials/examples/gateways/turbogears/mygateway.py @@ -1,6 +1,6 @@ import logging -from pyamf.remoting.gateway.wsgi import WSGIGateway +from src.pyamf.remoting import WSGIGateway logging.basicConfig( @@ -27,4 +27,4 @@ def scramble(self, text): # Expose our services services = {"Services" : Services()} -GatewayController = WSGIGateway(services, logger=logging, debug=True) \ No newline at end of file +GatewayController = WSGIGateway(services, logger=logging, debug=True) diff --git a/doc/tutorials/examples/gateways/twisted/client.py b/doc/tutorials/examples/gateways/twisted/client.py index c28f09f0..d472eeb4 100644 --- a/doc/tutorials/examples/gateways/twisted/client.py +++ b/doc/tutorials/examples/gateways/twisted/client.py @@ -1,7 +1,7 @@ -from pyamf.remoting.client import RemotingService +from src.pyamf.remoting import RemotingService import logging - + logging.basicConfig( level=logging.DEBUG, format='%(asctime)s %(levelname)-5.5s [%(name)s] %(message)s' @@ -15,4 +15,4 @@ print service1.testn('Hello World') service2 = client.getService('myadd') -print service2(1,2) \ No newline at end of file +print service2(1,2) diff --git a/doc/tutorials/examples/gateways/twisted/wsgi.py b/doc/tutorials/examples/gateways/twisted/wsgi.py index 2d1c3645..39b6cf37 100644 --- a/doc/tutorials/examples/gateways/twisted/wsgi.py +++ b/doc/tutorials/examples/gateways/twisted/wsgi.py @@ -1,6 +1,6 @@ import logging -from pyamf.remoting.gateway.wsgi import WSGIGateway +from src.pyamf.remoting import WSGIGateway from pyamf.remoting.gateway import expose_request from twisted.web import server @@ -9,7 +9,7 @@ from twisted.internet import reactor from twisted.application import service, strports - + logging.basicConfig( level=logging.DEBUG, format='%(asctime)s %(levelname)-5.5s [%(name)s] %(message)s' @@ -35,15 +35,15 @@ def testn(self, request, n): # A standalone function that can be bound to a service. -def add(a, b): - return a + b +def add(a, b): + return a + b # Create a dictionary mapping the service namespaces to a function # or class instance -services = { +services = { 'example': example(), - 'myadd': add + 'myadd': add } # Create and start a thread pool, @@ -64,4 +64,4 @@ def add(a, b): # Hooks for twistd application = service.Application('PyAMF Sample Remoting Server') -server.setServiceParent(application) \ No newline at end of file +server.setServiceParent(application) diff --git a/doc/tutorials/examples/gateways/werkzeug/client.py b/doc/tutorials/examples/gateways/werkzeug/client.py index b7d6d848..c47c862a 100644 --- a/doc/tutorials/examples/gateways/werkzeug/client.py +++ b/doc/tutorials/examples/gateways/werkzeug/client.py @@ -1,7 +1,7 @@ -from pyamf.remoting.client import RemotingService +from src.pyamf.remoting import RemotingService import logging - + logging.basicConfig( level=logging.DEBUG, format='%(asctime)s %(levelname)-5.5s [%(name)s] %(message)s' @@ -12,4 +12,4 @@ service = client.getService('echo') echo = service('Hello World!') -logging.debug(echo) \ No newline at end of file +logging.debug(echo) diff --git a/doc/tutorials/examples/general/client/authentication.py b/doc/tutorials/examples/general/client/authentication.py index 155bb6b9..7e2a604a 100644 --- a/doc/tutorials/examples/general/client/authentication.py +++ b/doc/tutorials/examples/general/client/authentication.py @@ -1,4 +1,4 @@ -from pyamf.remoting.client import RemotingService +from src.pyamf.remoting import RemotingService client = RemotingService('https://demo.pyamf.org/gateway/authentication') client.setCredentials('jane', 'doe') @@ -7,4 +7,4 @@ print service.sum(85, 115) # should print 200.0 client.setCredentials('abc', 'def') -print service.sum(85, 115).description # should print Authentication Failed \ No newline at end of file +print service.sum(85, 115).description # should print Authentication Failed diff --git a/doc/tutorials/examples/general/client/basic.py b/doc/tutorials/examples/general/client/basic.py index b680d89c..ed2166fb 100644 --- a/doc/tutorials/examples/general/client/basic.py +++ b/doc/tutorials/examples/general/client/basic.py @@ -1,6 +1,6 @@ -from pyamf.remoting.client import RemotingService +from src.pyamf.remoting import RemotingService client = RemotingService('http://demo.pyamf.org/gateway/recordset') service = client.getService('service') -print service.getLanguages() \ No newline at end of file +print service.getLanguages() diff --git a/doc/tutorials/examples/general/client/exception.py b/doc/tutorials/examples/general/client/exception.py index d5c5b315..7c5a736e 100644 --- a/doc/tutorials/examples/general/client/exception.py +++ b/doc/tutorials/examples/general/client/exception.py @@ -1,4 +1,4 @@ ->>> from pyamf.remoting.client import RemotingService +>>> from src.pyamf.remoting import RemotingService >>> gateway = RemotingService('http://example.org/gw') >>> service = gateway.getService('type_error') >>> service() diff --git a/doc/tutorials/examples/general/client/headers.py b/doc/tutorials/examples/general/client/headers.py index 67bde294..634434ff 100644 --- a/doc/tutorials/examples/general/client/headers.py +++ b/doc/tutorials/examples/general/client/headers.py @@ -1,4 +1,4 @@ -from pyamf.remoting.client import RemotingService +from src.pyamf.remoting import RemotingService gw = RemotingService('http://demo.pyamf.org/gateway/recordset') diff --git a/doc/tutorials/examples/general/client/logger.py b/doc/tutorials/examples/general/client/logger.py index 41caa14e..702358ae 100644 --- a/doc/tutorials/examples/general/client/logger.py +++ b/doc/tutorials/examples/general/client/logger.py @@ -1,4 +1,4 @@ -from pyamf.remoting.client import RemotingService +from src.pyamf.remoting import RemotingService import logging logging.basicConfig(level=logging.DEBUG, @@ -8,4 +8,4 @@ client = RemotingService(gateway, logger=logging) service = client.getService('service') -print service.getLanguages() \ No newline at end of file +print service.getLanguages() diff --git a/doc/tutorials/examples/general/client/referer.py b/doc/tutorials/examples/general/client/referer.py index 1a8d6c9d..5f7d0ee4 100644 --- a/doc/tutorials/examples/general/client/referer.py +++ b/doc/tutorials/examples/general/client/referer.py @@ -1,4 +1,4 @@ -from pyamf.remoting.client import RemotingService +from src.pyamf.remoting import RemotingService appReferer = 'client.py' gateway = 'http://demo.pyamf.org/gateway/helloworld' diff --git a/doc/tutorials/examples/general/client/user_agent.py b/doc/tutorials/examples/general/client/user_agent.py index 1ed57636..1800d031 100644 --- a/doc/tutorials/examples/general/client/user_agent.py +++ b/doc/tutorials/examples/general/client/user_agent.py @@ -1,4 +1,4 @@ -from pyamf.remoting.client import RemotingService +from src.pyamf.remoting import RemotingService appName = 'MyApp/0.1.0' gateway = 'http://demo.pyamf.org/gateway/helloworld' diff --git a/pyamf/util/imports.py b/pyamf/util/imports.py deleted file mode 100644 index 99b84717..00000000 --- a/pyamf/util/imports.py +++ /dev/null @@ -1,137 +0,0 @@ -# Copyright (c) The PyAMF Project. -# See LICENSE.txt for details. - -""" -Tools for doing dynamic imports. - -@since: 0.3 -""" - -import sys - - -__all__ = ['when_imported'] - - -def when_imported(name, *hooks): - """ - Call C{hook(module)} when module named C{name} is first imported. C{name} - must be a fully qualified (i.e. absolute) module name. - - C{hook} must accept one argument: which will be the imported module object. - - If the module has already been imported, 'hook(module)' is called - immediately, and the module object is returned from this function. If the - module has not been imported, then the hook is called when the module is - first imported. - """ - global finder - - finder.when_imported(name, *hooks) - - -class ModuleFinder(object): - """ - This is a special module finder object that executes a collection of - callables when a specific module has been imported. An instance of this - is placed in C{sys.meta_path}, which is consulted before C{sys.modules} - - allowing us to provide this functionality. - - @ivar post_load_hooks: C{dict} of C{full module path -> callable} to be - executed when the module is imported. - @ivar loaded_modules: C{list} of modules that this finder has seen. Used - to stop recursive imports in L{load_module} - @see: L{when_imported} - @since: 0.5 - """ - - def __init__(self): - self.post_load_hooks = {} - self.loaded_modules = [] - - def find_module(self, name, path=None): - """ - Called when an import is made. If there are hooks waiting for this - module to be imported then we stop the normal import process and - manually load the module. - - @param name: The name of the module being imported. - @param path The root path of the module (if a package). We ignore this. - @return: If we want to hook this module, we return a C{loader} - interface (which is this instance again). If not we return C{None} - to allow the standard import process to continue. - """ - if name in self.loaded_modules: - return None - - hooks = self.post_load_hooks.get(name, None) - - if hooks: - return self - - def load_module(self, name): - """ - If we get this far, then there are hooks waiting to be called on - import of this module. We manually load the module and then run the - hooks. - - @param name: The name of the module to import. - """ - self.loaded_modules.append(name) - - try: - __import__(name, {}, {}, []) - - mod = sys.modules[name] - self._run_hooks(name, mod) - except: - self.loaded_modules.pop() - - raise - - return mod - - def when_imported(self, name, *hooks): - """ - @see: L{when_imported} - """ - if name in sys.modules: - for hook in hooks: - hook(sys.modules[name]) - - return - - h = self.post_load_hooks.setdefault(name, []) - h.extend(hooks) - - def _run_hooks(self, name, module): - """ - Run all hooks for a module. - """ - hooks = self.post_load_hooks.pop(name, []) - - for hook in hooks: - hook(module) - - def __getstate__(self): - return (self.post_load_hooks.copy(), self.loaded_modules[:]) - - def __setstate__(self, state): - self.post_load_hooks, self.loaded_modules = state - - -def _init(): - """ - Internal function to install the module finder. - """ - global finder - - if finder is None: - finder = ModuleFinder() - - if finder not in sys.meta_path: - sys.meta_path.insert(0, finder) - - -finder = None -_init() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..9b534156 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,95 @@ +[build-system] +requires = [ + "setuptools>=81.0.0", + "wheel>=0.46.0", + "Cython>=3.1.0", +] +build-backend = "setuptools.build_meta" + +[project] +name = "Py3AMF" +version = "0.8.11" +description = "AMF support for Python" +readme = "README.rst" +requires-python = ">=3.12" +authors = [ + { name = "The Py3AMF Project", email = "yhbu@stdc.so" } +] +license = { text = "MIT License" } +keywords = [ + "python3", "amf", "amf0", "amf3", "flex", "flash", "remoting", "rpc", + "http", "flashplayer", "air", "bytearray", "objectproxy", "arraycollection", + "recordset", "actionscript", "decoder", "encoder", "gateway", "remoteobject", + "twisted", "pylons", "django", "sharedobject", "lso", "sol" +] +classifiers = [ + "Framework :: Django", + "Framework :: Pylons", + "Framework :: Twisted", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: C", + "Programming Language :: Python", + "Programming Language :: Cython", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + "Topic :: Software Development :: Libraries :: Python Modules", +] +dependencies = [ + "defusedxml", +] + +[project.urls] +Homepage = "https://github.com/StdCarrot/Py3AMF" + +[project.optional-dependencies] +twisted = ["Twisted>=16.0.0"] +django = ["Django>=0.96"] +sqlalchemy = ["SQLAlchemy>=0.4"] +elixir = ["Elixir>=0.7.1"] +lxml = ["lxml>=4.4.0"] +six = ["six>=1.10.0"] +test = ["pytest"] + + + +[tool.epydoc] +# The documented project's name. +name = "PyAMF API Documentation" +# The documented project's URL. +url = "http://pyamf.org/" +# The list of objects to document. +modules = "pyamf" +# Don't examine in any way the modules whose dotted name match this +# regular expression pattern. +exclude = "tests*" +# The "top" page for the documentation. Can be a URL, the name +# of a module or class, or one of the special names "trees.html", +# "indices.html", or "help.html" +top = "pyamf" +# The type of output that should be generated. Should be one +# of: html, text, latex, dvi, ps, pdf. +output = "html" +# The path to the output directory. May be relative or absolute. +target = "doc/build/api" +# If True, don't try to use colors or cursor control when doing +# textual output. The default False assumes a rich text prompt. +simple-term = 0 +# An integer indicating how verbose epydoc should be. The default +# value is 0; negative values will supress warnings and errors; +# positive values will give more verbose output. +verbosity = 0 +# Whether or not to list each module's imports. +imports = true +# Whether or not to include syntax highlighted source code in +# the output (HTML only). +sourcecode = true + +[tool.flake8] +exclude = ["doc", ".ropeproject"] +ignore = ["E402"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 36969f2c..00000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -defusedxml diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index e73a67c7..00000000 --- a/setup.cfg +++ /dev/null @@ -1,55 +0,0 @@ -[epydoc] -# The documented project's name. -name: PyAMF API Documentation - -# The documented project's URL. -url: http://pyamf.org/ - -# The list of objects to document. -modules: pyamf - -# Don't examine in any way the modules whose dotted name match this -# regular expression pattern. -exclude: tests* - -# The "top" page for the documentation. Can be a URL, the name -# of a module or class, or one of the special names "trees.html", -# "indices.html", or "help.html" -top: pyamf - -# The type of output that should be generated. Should be one -# of: html, text, latex, dvi, ps, pdf. -output: html - -# The path to the output directory. May be relative or absolute. -target: doc/build/api - -# If True, don't try to use colors or cursor control when doing -# textual output. The default False assumes a rich text prompt. -simple-term: 0 - -# An integer indicating how verbose epydoc should be. The default -# value is 0; negative values will supress warnings and errors; -# positive values will give more verbose output. -verbosity: 0 - -# Whether or not to list each module's imports. -imports: yes - -# Whether or not to include syntax highlighted source code in -# the output (HTML only). -sourcecode: yes - -# The list of graph types that should be automatically included -# in the output. Graphs are generated using the Graphviz "dot" -# executable. Graph types include: "classtree", "callgraph", -# "umlclasstree". Use "all" to include all graph types. -#graph: umlclasstree - -# The path to the Graphviz "dot" executable, used to generate -# graphs. -#dotpath: /usr/local/bin/dot - -[flake8] -exclude=doc,.ropeproject -ignore=E402 diff --git a/setup.py b/setup.py index 39ecd245..12b9075e 100644 --- a/setup.py +++ b/setup.py @@ -4,74 +4,162 @@ # See LICENSE.txt for details. # import ordering is important -import setupinfo + +import os.path +import platform +from enum import Enum +from os import environ + from setuptools import setup, find_packages +try: + from Cython.Distutils import build_ext + + have_cython = True +except ImportError: + from setuptools.command.build_ext import build_ext + + have_cython = False + + +from setuptools import Extension + version = (0, 8, 11) -name = "Py3AMF" -description = "AMF support for Python" -long_description = setupinfo.read('README.rst') -url = "https://github.com/StdCarrot/Py3AMF" -author = "The Py3AMF Project" -author_email = "yhbu@stdc.so" -license = "MIT License" - -classifiers = """ -Framework :: Django -Framework :: Pylons -Framework :: Twisted -Intended Audience :: Developers -Intended Audience :: Information Technology -License :: OSI Approved :: MIT License -Natural Language :: English -Operating System :: OS Independent -Programming Language :: C -Programming Language :: Python -Programming Language :: Cython -Programming Language :: Python :: 3.5 -Programming Language :: Python :: 3.6 -Programming Language :: Python :: 3.7 -Programming Language :: Python :: 3.8 -Topic :: Internet :: WWW/HTTP :: WSGI :: Application -Topic :: Software Development :: Libraries :: Python Modules -""" -keywords = """ -python3 amf amf0 amf3 flex flash remoting rpc http flashplayer air bytearray -objectproxy arraycollection recordset actionscript decoder encoder gateway -remoteobject twisted pylons django sharedobject lso sol -""" +class EnvConfig(Enum): + """Custom environment flags and configuration values. + When reading boolean values, the following values are considered truthy: -def setup_package(): - setupinfo.set_version(version) + - `true` + - `yes` + - `1` + - `on` + + The evaluation is case-insensitive. + + """ - setupinfo.write_version_py() + DISABLE_EXT = "PYAMF_DISABLE_EXT" + """If set to truthy value, disables the building of the C extensions""" + + def read_from_env(self, fallback: bool = False) -> bool: + return self.read_bool_like_val(self.value, fallback) + + @classmethod + def read_bool_like_val(cls, key: str, fallback: bool = False) -> bool: + val = environ.get(key) + if val is None: + return fallback + val = val.lower() + return val in ('true', 'yes', '1', 'on') + + +def setup_package(): + write_version_py() setup( - name=name, - version=setupinfo.get_version(), - description=description, - long_description=long_description, - url=url, - author=author, - author_email=author_email, - keywords=keywords.strip(), + version=get_version(), license=license, - packages=find_packages(), - ext_modules=setupinfo.get_extensions(), - install_requires=setupinfo.get_install_requirements(), - tests_require=setupinfo.get_test_requirements(), - test_suite="pyamf.tests.get_suite", + package_dir={"": "src"}, + packages=find_packages("src"), + ext_modules=get_binary_extensions(), zip_safe=False, - extras_require=setupinfo.get_extras_require(), - classifiers=( - [_f for _f in classifiers.strip().split('\n') if _f] + - setupinfo.get_trove_classifiers() + **extra_setup_args()) + + + +can_compile_extensions = have_cython and platform.python_implementation() == "CPython" + + +def get_binary_extensions() -> list[Extension]: + + if not can_compile_extensions or EnvConfig.DISABLE_EXT.read_from_env(): + return [] + + extensions = [ + Extension( + name="cpyamf.amf0", + sources=["src/cpyamf/amf0.pyx"], + include_dirs=["src"], + ), + Extension( + name="cpyamf.amf3", + sources=["src/cpyamf/amf3.pyx"], + include_dirs=["src"], + ), + Extension( + name="cpyamf.codec", + sources=["src/cpyamf/codec.pyx"], + include_dirs=["src"], ), - **setupinfo.extra_setup_args()) + Extension( + name="cpyamf.util", + sources=["src/cpyamf/util.pyx"], + include_dirs=["src"], + ), + ] + + return extensions + + +def get_version(): + v = '' + prev = None + + for x in version: + if prev is not None: + if isinstance(x, int): + v += '.' + + prev = x + v += str(x) + + return v.strip('.') + + +def get_package_data(): + return { + 'cpyamf': ['*.pxd'], + } + + +def extra_setup_args(): + """ + Extra kwargs to supply in the call to C{setup}. + + This is used to supply custom commands, not metadata - that should live in + setup.py itself. + """ + return { + 'package_data': get_package_data(), + } + + +def write_version_py(filename='src/pyamf/_version.py'): + """ + """ + if os.path.exists(filename): + os.remove(filename) + + content = """\ +# THIS FILE IS GENERATED BY PYAMF SETUP.PY +from pyamf.versions import Version + +version = Version(*%(version)r) +""" + a = open(filename, 'wt') + + try: + a.write(content % {'version': version}) + finally: + a.close() + + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() if __name__ == '__main__': diff --git a/setupinfo.py b/setupinfo.py deleted file mode 100644 index a36aa589..00000000 --- a/setupinfo.py +++ /dev/null @@ -1,300 +0,0 @@ -# Copyright (c) The PyAMF Project. -# See LICENSE.txt for details. - -""" -Meta data and helper functions for setup -""" - -import fnmatch -import os.path -import platform -import sys - -try: - from Cython.Distutils import build_ext - - have_cython = True -except ImportError: - from setuptools.command.build_ext import build_ext - - have_cython = False - - -from setuptools.command import test, sdist -from setuptools import Extension -from distutils.core import Distribution - - -_version = None - -can_compile_extensions = platform.python_implementation() == "CPython" - - -class MyDistribution(Distribution): - """ - This seems to be is the only obvious way to add a global option to - distutils. - - Provide the ability to disable building the extensions for any called - command. - """ - - global_options = Distribution.global_options + [ - ('disable-ext', None, 'Disable building extensions.') - ] - - def finalize_options(self): - Distribution.finalize_options(self) - - try: - i = self.script_args.index('--disable-ext') - except ValueError: - self.disable_ext = False - else: - self.disable_ext = True - self.script_args.pop(i) - - -class MyBuildExt(build_ext): - """ - The companion to L{MyDistribution} that checks to see if building the - extensions are disabled. - """ - - def run(self, *args, **kwargs): - if self.distribution.disable_ext: - return - - build_ext.run(self, *args, **kwargs) - - -class MySDist(sdist.sdist): - """ - We generate the Cython code for a source distribution - """ - - def cythonise(self): - ext = MyBuildExt(self.distribution) - ext.initialize_options() - ext.finalize_options() - - ext.check_extensions_list(ext.extensions) - - for e in ext.extensions: - e.sources = ext.cython_sources(e.sources, e) - - def run(self): - if not have_cython: - print('ERROR - Cython is required to build source distributions') - - raise SystemExit(1) - - self.cythonise() - - return sdist.sdist.run(self) - - -class TestCommand(test.test): - """ - Ensures that unittest2 is imported if required and replaces the old - unittest module. - """ - - def run_tests(self): - try: - import unittest2 - - sys.modules['unittest'] = unittest2 - except ImportError: - pass - - return test.test.run_tests(self) - - -def set_version(version): - global _version - - _version = version - - -def get_version(): - v = '' - prev = None - - for x in _version: - if prev is not None: - if isinstance(x, int): - v += '.' - - prev = x - v += str(x) - - return v.strip('.') - - -def get_extras_require(): - return { - 'twisted': ['Twisted>=16.0.0'], - 'django': ['Django>=0.96'], - 'sqlalchemy': ['SQLAlchemy>=0.4'], - 'elixir': ['Elixir>=0.7.1'], - 'lxml': ['lxml>=4.4.0'], - 'six': ['six>=1.10.0'] - } - - -def get_package_data(): - return { - 'cpyamf': ['*.pxd'], - } - - -def extra_setup_args(): - """ - Extra kwargs to supply in the call to C{setup}. - - This is used to supply custom commands, not metadata - that should live in - setup.py itself. - """ - return { - 'distclass': MyDistribution, - 'cmdclass': { - 'test': TestCommand, - 'build_ext': MyBuildExt, - 'sdist': MySDist - }, - 'package_data': get_package_data(), - } - - -def get_install_requirements(): - """ - Returns a list of dependencies for PyAMF to function correctly on the - target platform. - """ - install_requires = ['defusedxml'] - - if 'dev' in get_version(): - if can_compile_extensions: - install_requires.extend(['Cython>=0.28']) - - return install_requires - - -def get_test_requirements(): - """ - Returns a list of required packages to run the test suite. - """ - tests_require = [] - - return tests_require - - -def write_version_py(filename='pyamf/_version.py'): - """ - """ - if os.path.exists(filename): - os.remove(filename) - - content = """\ -# THIS FILE IS GENERATED BY PYAMF SETUP.PY -from pyamf.versions import Version - -version = Version(*%(version)r) -""" - a = open(filename, 'wt') - - try: - a.write(content % {'version': _version}) - finally: - a.close() - - -def make_extension(mod_name, **extra_options): - """ - Tries is best to return an Extension instance based on the mod_name - """ - base_name = os.path.join(mod_name.replace('.', os.path.sep)) - - if have_cython: - for ext in ['.pyx', '.py']: - source = base_name + ext - - if os.path.exists(source): - return Extension(mod_name, [source], **extra_options) - - print('WARNING: Could not find Cython source for %r' % (mod_name,)) - else: - source = base_name + '.c' - - if os.path.exists(source): - return Extension(mod_name, [source], **extra_options) - - print('WARNING: Could not build extension for %r, no source found' % ( - mod_name,)) - - -def read(fname): - return open(os.path.join(os.path.dirname(__file__), fname)).read() - - -def get_extensions(): - """ - Return a list of Extension instances that can be compiled. - """ - if not can_compile_extensions: - # due to changes in pip these prints have no effect - print(80 * '*') - print('WARNING:') - print( - '\tAn optional code optimization (C extension) could not be ' - 'compiled.\n\n' - ) - print('\tOptimizations for this package will not be available!\n\n') - print('Compiling extensions is not supported on %r' % (sys.platform,)) - print(80 * '*') - - return [] - - extensions = [] - - for p in recursive_glob('.', '*.pyx'): - mod_name = os.path.splitext(p)[0].replace(os.path.sep, '.') - - e = make_extension(mod_name) - - if e: - extensions.append(e) - - return extensions - - -def get_trove_classifiers(): - """ - Return a list of trove classifiers that are setup dependent. - """ - classifiers = [] - - def dev_status(): - version = get_version() - - if 'dev' in version: - return 'Development Status :: 2 - Pre-Alpha' - elif 'alpha' in version: - return 'Development Status :: 3 - Alpha' - elif 'beta' in version: - return 'Development Status :: 4 - Beta' - else: - return 'Development Status :: 5 - Production/Stable' - - return classifiers + [dev_status()] - - -def recursive_glob(path, pattern): - matches = [] - - for root, dirnames, filenames in os.walk(path): - for filename in fnmatch.filter(filenames, pattern): - matches.append(os.path.normpath(os.path.join(root, filename))) - - return matches diff --git a/cpyamf/__init__.py b/src/cpyamf/__init__.py similarity index 100% rename from cpyamf/__init__.py rename to src/cpyamf/__init__.py diff --git a/cpyamf/amf0.pxd b/src/cpyamf/amf0.pxd similarity index 95% rename from cpyamf/amf0.pxd rename to src/cpyamf/amf0.pxd index 8c1c53da..f6e9fb8a 100644 --- a/cpyamf/amf0.pxd +++ b/src/cpyamf/amf0.pxd @@ -1,4 +1,4 @@ -from cpyamf cimport codec, util, amf3 +from cpyamf cimport codec, amf3 cdef class Context(codec.Context): diff --git a/cpyamf/amf0.pyx b/src/cpyamf/amf0.pyx similarity index 99% rename from cpyamf/amf0.pyx rename to src/cpyamf/amf0.pyx index b0ca1dde..54fa8d22 100644 --- a/cpyamf/amf0.pyx +++ b/src/cpyamf/amf0.pyx @@ -573,7 +573,7 @@ cdef class Encoder(codec.Encoder): try: # list comprehensions to save the day max_index = max([y[0] for y in o.items() - if isinstance(y[0], (int, long))]) + if isinstance(y[0], int)]) if max_index < 0: max_index = 0 diff --git a/cpyamf/amf3.pxd b/src/cpyamf/amf3.pxd similarity index 100% rename from cpyamf/amf3.pxd rename to src/cpyamf/amf3.pxd diff --git a/cpyamf/amf3.pyx b/src/cpyamf/amf3.pyx similarity index 98% rename from cpyamf/amf3.pyx rename to src/cpyamf/amf3.pyx index c8d8ec6e..f487f8b9 100644 --- a/cpyamf/amf3.pyx +++ b/src/cpyamf/amf3.pyx @@ -12,12 +12,10 @@ from libc.stdlib cimport malloc, free cimport cython -from cpyamf.util cimport cBufferedByteStream, BufferedByteStream +from cpyamf.util cimport cBufferedByteStream from cpyamf cimport codec import pyamf from pyamf import util, amf3, xml -import types - try: import zlib @@ -352,7 +350,7 @@ cdef class Decoder(codec.Decoder): size >>= 1 key = self.readString() - if PyUnicode_GetSize(key) == 0: + if len(key) == 0: # integer indexes only -> python list result = [] self.context.addObject(result) @@ -365,7 +363,7 @@ cdef class Decoder(codec.Decoder): tmp = pyamf.MixedArray() self.context.addObject(tmp) - while PyUnicode_GetSize(key): + while len(key): tmp[key] = self.readElement() key = self.readString() @@ -388,7 +386,7 @@ cdef class Decoder(codec.Decoder): cdef object alias = None cdef Py_ssize_t i - if PyUnicode_GET_SIZE(name) == 0: + if PyUnicode_GET_LENGTH(name) == 0: name = pyamf.ASObject try: @@ -633,7 +631,7 @@ cdef class Encoder(codec.Encoder): cdef bint is_unicode = 0 if PyUnicode_Check(u): - l = PyUnicode_GET_SIZE(u) + l = PyUnicode_GET_LENGTH(u) is_unicode = 1 elif PyBytes_Check(u): l = PyBytes_GET_SIZE(u) @@ -809,7 +807,8 @@ cdef class Encoder(codec.Encoder): self.stream.write(&REF_CHAR, 1) for key, value in obj.items(): - if PyInt_Check(key) or PyLong_Check(key): + # if PyInt_Check(key) or PyLong_Check(key): + if PyLong_Check(key): key = str(key) self.serialiseString(key) @@ -842,7 +841,7 @@ cdef class Encoder(codec.Encoder): str_keys = [] for x in keys: - if isinstance(x, (int, long)): + if isinstance(x, int): int_keys.append(x) elif isinstance(x, (str, unicode)): str_keys.append(x) diff --git a/cpyamf/codec.pxd b/src/cpyamf/codec.pxd similarity index 100% rename from cpyamf/codec.pxd rename to src/cpyamf/codec.pxd diff --git a/cpyamf/codec.pyx b/src/cpyamf/codec.pyx similarity index 99% rename from cpyamf/codec.pyx rename to src/cpyamf/codec.pyx index c7cf5dc2..2eea45df 100644 --- a/cpyamf/codec.pyx +++ b/src/cpyamf/codec.pyx @@ -129,7 +129,7 @@ cdef class IndexedCollection(object): if p is None: return -1 - return PyInt_AS_LONG(p) + return PyLong_AsSsize_t(p) cpdef Py_ssize_t append(self, object obj) except -1: self._increase_size() diff --git a/cpyamf/util.pxd b/src/cpyamf/util.pxd similarity index 100% rename from cpyamf/util.pxd rename to src/cpyamf/util.pxd diff --git a/cpyamf/util.pyx b/src/cpyamf/util.pyx similarity index 97% rename from cpyamf/util.pyx rename to src/cpyamf/util.pyx index 60450553..f00c228f 100644 --- a/cpyamf/util.pyx +++ b/src/cpyamf/util.pyx @@ -17,10 +17,10 @@ cdef extern from "stdio.h": int SIZEOF_LONG cdef extern from "Python.h": - int _PyFloat_Pack4(float, unsigned char *, int) except? -1 - int _PyFloat_Pack8(double, unsigned char *, int) except? -1 - double _PyFloat_Unpack4(unsigned char *, int) except? -1.0 - double _PyFloat_Unpack8(unsigned char *, int) except? -1.0 + int PyFloat_Pack4(float, unsigned char *, int) except? -1 + int PyFloat_Pack8(double, unsigned char *, int) except? -1 + double PyFloat_Unpack4(unsigned char *, int) except? -1.0 + double PyFloat_Unpack8(unsigned char *, int) except? -1.0 from pyamf import python @@ -93,21 +93,21 @@ cdef int build_platform_exceptional_floats() except -1: if float_broken == 1: try: - _PyFloat_Unpack8(&NaN, not is_big_endian(SYSTEM_ENDIAN)) + PyFloat_Unpack8(&NaN, not is_big_endian(SYSTEM_ENDIAN)) except: raise else: memcpy(&platform_nan, &system_nan, 8) try: - _PyFloat_Unpack8(&PosInf, not is_big_endian(SYSTEM_ENDIAN)) + PyFloat_Unpack8(&PosInf, not is_big_endian(SYSTEM_ENDIAN)) except: raise else: memcpy(&platform_posinf, &system_posinf, 8) try: - _PyFloat_Unpack8(&NegInf, not is_big_endian(SYSTEM_ENDIAN)) + PyFloat_Unpack8(&NegInf, not is_big_endian(SYSTEM_ENDIAN)) except: raise else: @@ -173,7 +173,7 @@ cdef inline int swap_bytes(unsigned char *buffer, Py_ssize_t size) nogil: cdef bint is_broken_float() except -1: - cdef double test = _PyFloat_Unpack8(NaN, 0) + cdef double test = PyFloat_Unpack8(NaN, 0) cdef int result cdef unsigned char *buf = &test @@ -795,7 +795,7 @@ cdef class cBufferedByteStream(object): if swap_bytes(buf, 8) == -1: PyErr_NoMemory() - obj[0] = _PyFloat_Unpack8(buf, not is_big_endian(self.endian)) + obj[0] = PyFloat_Unpack8(buf, not is_big_endian(self.endian)) return 0 @@ -841,7 +841,7 @@ cdef class cBufferedByteStream(object): PyErr_NoMemory() if done == 0: - _PyFloat_Pack8(val, buf, not is_big_endian(self.endian)) + PyFloat_Pack8(val, buf, not is_big_endian(self.endian)) self.write(buf, 8) finally: @@ -858,7 +858,7 @@ cdef class cBufferedByteStream(object): self.read(&buf, 4) - x[0] = _PyFloat_Unpack4(buf, le) + x[0] = PyFloat_Unpack4(buf, le) return 0 @@ -878,7 +878,7 @@ cdef class cBufferedByteStream(object): PyErr_NoMemory() try: - _PyFloat_Pack4(c, buf, le) + PyFloat_Pack4(c, buf, le) self.write(buf, 4) finally: diff --git a/pyamf/__init__.py b/src/pyamf/__init__.py similarity index 98% rename from pyamf/__init__.py rename to src/pyamf/__init__.py index 626b81fe..cf6c5c5d 100644 --- a/pyamf/__init__.py +++ b/src/pyamf/__init__.py @@ -13,6 +13,8 @@ import inspect +import importlib + from pyamf import util, _version from pyamf.adapters import register_adapters, get_adapter from pyamf import python @@ -484,15 +486,13 @@ def _get_amf_module(version, use_ext=None): if use_ext is None: # try to use the extension but fallback gracefully try: - module = __import__('cpyamf.' + module_name) + return importlib.import_module('cpyamf.' + module_name) except ImportError: - module = __import__('pyamf.' + module_name) + return importlib.import_module('pyamf.' + module_name) elif not use_ext: - module = __import__('pyamf.' + module_name) + return importlib.import_module('pyamf.' + module_name) else: - module = __import__('cpyamf.' + module_name) - - return getattr(module, module_name) + return importlib.import_module('cpyamf.' + module_name) def get_decoder(encoding, *args, **kwargs): diff --git a/pyamf/adapters/__init__.py b/src/pyamf/adapters/__init__.py similarity index 100% rename from pyamf/adapters/__init__.py rename to src/pyamf/adapters/__init__.py diff --git a/pyamf/adapters/_array.py b/src/pyamf/adapters/_array.py similarity index 100% rename from pyamf/adapters/_array.py rename to src/pyamf/adapters/_array.py diff --git a/pyamf/adapters/_collections.py b/src/pyamf/adapters/_collections.py similarity index 100% rename from pyamf/adapters/_collections.py rename to src/pyamf/adapters/_collections.py diff --git a/pyamf/adapters/_decimal.py b/src/pyamf/adapters/_decimal.py similarity index 100% rename from pyamf/adapters/_decimal.py rename to src/pyamf/adapters/_decimal.py diff --git a/pyamf/adapters/_django_contrib_auth_models.py b/src/pyamf/adapters/_django_contrib_auth_models.py similarity index 100% rename from pyamf/adapters/_django_contrib_auth_models.py rename to src/pyamf/adapters/_django_contrib_auth_models.py diff --git a/pyamf/adapters/_django_db_models_base.py b/src/pyamf/adapters/_django_db_models_base.py similarity index 100% rename from pyamf/adapters/_django_db_models_base.py rename to src/pyamf/adapters/_django_db_models_base.py diff --git a/pyamf/adapters/_django_db_models_fields.py b/src/pyamf/adapters/_django_db_models_fields.py similarity index 100% rename from pyamf/adapters/_django_db_models_fields.py rename to src/pyamf/adapters/_django_db_models_fields.py diff --git a/pyamf/adapters/_django_db_models_query.py b/src/pyamf/adapters/_django_db_models_query.py similarity index 100% rename from pyamf/adapters/_django_db_models_query.py rename to src/pyamf/adapters/_django_db_models_query.py diff --git a/pyamf/adapters/_django_utils_translation.py b/src/pyamf/adapters/_django_utils_translation.py similarity index 100% rename from pyamf/adapters/_django_utils_translation.py rename to src/pyamf/adapters/_django_utils_translation.py diff --git a/pyamf/adapters/_elixir.py b/src/pyamf/adapters/_elixir.py similarity index 100% rename from pyamf/adapters/_elixir.py rename to src/pyamf/adapters/_elixir.py diff --git a/pyamf/adapters/_google_appengine_api_datastore_types.py b/src/pyamf/adapters/_google_appengine_api_datastore_types.py similarity index 100% rename from pyamf/adapters/_google_appengine_api_datastore_types.py rename to src/pyamf/adapters/_google_appengine_api_datastore_types.py diff --git a/pyamf/adapters/_google_appengine_ext_blobstore.py b/src/pyamf/adapters/_google_appengine_ext_blobstore.py similarity index 100% rename from pyamf/adapters/_google_appengine_ext_blobstore.py rename to src/pyamf/adapters/_google_appengine_ext_blobstore.py diff --git a/pyamf/adapters/_google_appengine_ext_db.py b/src/pyamf/adapters/_google_appengine_ext_db.py similarity index 100% rename from pyamf/adapters/_google_appengine_ext_db.py rename to src/pyamf/adapters/_google_appengine_ext_db.py diff --git a/pyamf/adapters/_google_appengine_ext_ndb.py b/src/pyamf/adapters/_google_appengine_ext_ndb.py similarity index 100% rename from pyamf/adapters/_google_appengine_ext_ndb.py rename to src/pyamf/adapters/_google_appengine_ext_ndb.py diff --git a/pyamf/adapters/_sets.py b/src/pyamf/adapters/_sets.py similarity index 100% rename from pyamf/adapters/_sets.py rename to src/pyamf/adapters/_sets.py diff --git a/pyamf/adapters/_sqlalchemy_orm.py b/src/pyamf/adapters/_sqlalchemy_orm.py similarity index 100% rename from pyamf/adapters/_sqlalchemy_orm.py rename to src/pyamf/adapters/_sqlalchemy_orm.py diff --git a/pyamf/adapters/_sqlalchemy_orm_collections.py b/src/pyamf/adapters/_sqlalchemy_orm_collections.py similarity index 100% rename from pyamf/adapters/_sqlalchemy_orm_collections.py rename to src/pyamf/adapters/_sqlalchemy_orm_collections.py diff --git a/pyamf/adapters/_weakref.py b/src/pyamf/adapters/_weakref.py similarity index 100% rename from pyamf/adapters/_weakref.py rename to src/pyamf/adapters/_weakref.py diff --git a/pyamf/adapters/gae_base.py b/src/pyamf/adapters/gae_base.py similarity index 100% rename from pyamf/adapters/gae_base.py rename to src/pyamf/adapters/gae_base.py diff --git a/pyamf/adapters/models.py b/src/pyamf/adapters/models.py similarity index 100% rename from pyamf/adapters/models.py rename to src/pyamf/adapters/models.py diff --git a/pyamf/adapters/tests/__init__.py b/src/pyamf/adapters/tests/__init__.py similarity index 100% rename from pyamf/adapters/tests/__init__.py rename to src/pyamf/adapters/tests/__init__.py diff --git a/pyamf/adapters/tests/django_app/__init__.py b/src/pyamf/adapters/tests/django_app/__init__.py similarity index 100% rename from pyamf/adapters/tests/django_app/__init__.py rename to src/pyamf/adapters/tests/django_app/__init__.py diff --git a/pyamf/adapters/tests/django_app/adapters/__init__.py b/src/pyamf/adapters/tests/django_app/adapters/__init__.py similarity index 100% rename from pyamf/adapters/tests/django_app/adapters/__init__.py rename to src/pyamf/adapters/tests/django_app/adapters/__init__.py diff --git a/pyamf/adapters/tests/django_app/adapters/models.py b/src/pyamf/adapters/tests/django_app/adapters/models.py similarity index 100% rename from pyamf/adapters/tests/django_app/adapters/models.py rename to src/pyamf/adapters/tests/django_app/adapters/models.py diff --git a/pyamf/adapters/tests/django_app/settings.py b/src/pyamf/adapters/tests/django_app/settings.py similarity index 100% rename from pyamf/adapters/tests/django_app/settings.py rename to src/pyamf/adapters/tests/django_app/settings.py diff --git a/pyamf/adapters/tests/google/__init__.py b/src/pyamf/adapters/tests/google/__init__.py similarity index 100% rename from pyamf/adapters/tests/google/__init__.py rename to src/pyamf/adapters/tests/google/__init__.py diff --git a/pyamf/adapters/tests/google/_ndb_models.py b/src/pyamf/adapters/tests/google/_ndb_models.py similarity index 100% rename from pyamf/adapters/tests/google/_ndb_models.py rename to src/pyamf/adapters/tests/google/_ndb_models.py diff --git a/pyamf/adapters/tests/google/_xdb_models.py b/src/pyamf/adapters/tests/google/_xdb_models.py similarity index 100% rename from pyamf/adapters/tests/google/_xdb_models.py rename to src/pyamf/adapters/tests/google/_xdb_models.py diff --git a/pyamf/adapters/tests/google/test_blobstore.py b/src/pyamf/adapters/tests/google/test_blobstore.py similarity index 100% rename from pyamf/adapters/tests/google/test_blobstore.py rename to src/pyamf/adapters/tests/google/test_blobstore.py diff --git a/pyamf/adapters/tests/google/test_datastore_types.py b/src/pyamf/adapters/tests/google/test_datastore_types.py similarity index 100% rename from pyamf/adapters/tests/google/test_datastore_types.py rename to src/pyamf/adapters/tests/google/test_datastore_types.py diff --git a/pyamf/adapters/tests/google/test_ndb.py b/src/pyamf/adapters/tests/google/test_ndb.py similarity index 100% rename from pyamf/adapters/tests/google/test_ndb.py rename to src/pyamf/adapters/tests/google/test_ndb.py diff --git a/pyamf/adapters/tests/google/test_xdb.py b/src/pyamf/adapters/tests/google/test_xdb.py similarity index 100% rename from pyamf/adapters/tests/google/test_xdb.py rename to src/pyamf/adapters/tests/google/test_xdb.py diff --git a/pyamf/adapters/tests/test_array.py b/src/pyamf/adapters/tests/test_array.py similarity index 100% rename from pyamf/adapters/tests/test_array.py rename to src/pyamf/adapters/tests/test_array.py diff --git a/pyamf/adapters/tests/test_collections.py b/src/pyamf/adapters/tests/test_collections.py similarity index 100% rename from pyamf/adapters/tests/test_collections.py rename to src/pyamf/adapters/tests/test_collections.py diff --git a/pyamf/adapters/tests/test_django.py b/src/pyamf/adapters/tests/test_django.py similarity index 100% rename from pyamf/adapters/tests/test_django.py rename to src/pyamf/adapters/tests/test_django.py diff --git a/pyamf/adapters/tests/test_elixir.py b/src/pyamf/adapters/tests/test_elixir.py similarity index 100% rename from pyamf/adapters/tests/test_elixir.py rename to src/pyamf/adapters/tests/test_elixir.py diff --git a/pyamf/adapters/tests/test_sqlalchemy.py b/src/pyamf/adapters/tests/test_sqlalchemy.py similarity index 100% rename from pyamf/adapters/tests/test_sqlalchemy.py rename to src/pyamf/adapters/tests/test_sqlalchemy.py diff --git a/pyamf/adapters/tests/test_weakref.py b/src/pyamf/adapters/tests/test_weakref.py similarity index 100% rename from pyamf/adapters/tests/test_weakref.py rename to src/pyamf/adapters/tests/test_weakref.py diff --git a/pyamf/adapters/util.py b/src/pyamf/adapters/util.py similarity index 100% rename from pyamf/adapters/util.py rename to src/pyamf/adapters/util.py diff --git a/pyamf/alias.py b/src/pyamf/alias.py similarity index 100% rename from pyamf/alias.py rename to src/pyamf/alias.py diff --git a/pyamf/amf0.py b/src/pyamf/amf0.py similarity index 100% rename from pyamf/amf0.py rename to src/pyamf/amf0.py diff --git a/pyamf/amf3.py b/src/pyamf/amf3.py similarity index 100% rename from pyamf/amf3.py rename to src/pyamf/amf3.py diff --git a/pyamf/codec.py b/src/pyamf/codec.py similarity index 100% rename from pyamf/codec.py rename to src/pyamf/codec.py diff --git a/pyamf/flex/__init__.py b/src/pyamf/flex/__init__.py similarity index 100% rename from pyamf/flex/__init__.py rename to src/pyamf/flex/__init__.py diff --git a/pyamf/flex/data.py b/src/pyamf/flex/data.py similarity index 100% rename from pyamf/flex/data.py rename to src/pyamf/flex/data.py diff --git a/pyamf/flex/messaging.py b/src/pyamf/flex/messaging.py similarity index 100% rename from pyamf/flex/messaging.py rename to src/pyamf/flex/messaging.py diff --git a/pyamf/python.py b/src/pyamf/python.py similarity index 100% rename from pyamf/python.py rename to src/pyamf/python.py diff --git a/pyamf/remoting/__init__.py b/src/pyamf/remoting/__init__.py similarity index 100% rename from pyamf/remoting/__init__.py rename to src/pyamf/remoting/__init__.py diff --git a/pyamf/remoting/amf0.py b/src/pyamf/remoting/amf0.py similarity index 100% rename from pyamf/remoting/amf0.py rename to src/pyamf/remoting/amf0.py diff --git a/pyamf/remoting/amf3.py b/src/pyamf/remoting/amf3.py similarity index 100% rename from pyamf/remoting/amf3.py rename to src/pyamf/remoting/amf3.py diff --git a/pyamf/remoting/client/__init__.py b/src/pyamf/remoting/client/__init__.py similarity index 100% rename from pyamf/remoting/client/__init__.py rename to src/pyamf/remoting/client/__init__.py diff --git a/pyamf/remoting/gateway/__init__.py b/src/pyamf/remoting/gateway/__init__.py similarity index 100% rename from pyamf/remoting/gateway/__init__.py rename to src/pyamf/remoting/gateway/__init__.py diff --git a/pyamf/remoting/gateway/django.py b/src/pyamf/remoting/gateway/django.py similarity index 100% rename from pyamf/remoting/gateway/django.py rename to src/pyamf/remoting/gateway/django.py diff --git a/pyamf/remoting/gateway/google.py b/src/pyamf/remoting/gateway/google.py similarity index 100% rename from pyamf/remoting/gateway/google.py rename to src/pyamf/remoting/gateway/google.py diff --git a/pyamf/remoting/gateway/twisted.py b/src/pyamf/remoting/gateway/twisted.py similarity index 100% rename from pyamf/remoting/gateway/twisted.py rename to src/pyamf/remoting/gateway/twisted.py diff --git a/pyamf/remoting/gateway/wsgi.py b/src/pyamf/remoting/gateway/wsgi.py similarity index 100% rename from pyamf/remoting/gateway/wsgi.py rename to src/pyamf/remoting/gateway/wsgi.py diff --git a/pyamf/sol.py b/src/pyamf/sol.py similarity index 100% rename from pyamf/sol.py rename to src/pyamf/sol.py diff --git a/pyamf/tests/__init__.py b/src/pyamf/tests/__init__.py similarity index 100% rename from pyamf/tests/__init__.py rename to src/pyamf/tests/__init__.py diff --git a/pyamf/tests/gateway/__init__.py b/src/pyamf/tests/gateway/__init__.py similarity index 100% rename from pyamf/tests/gateway/__init__.py rename to src/pyamf/tests/gateway/__init__.py diff --git a/pyamf/tests/gateway/test_django.py b/src/pyamf/tests/gateway/test_django.py similarity index 100% rename from pyamf/tests/gateway/test_django.py rename to src/pyamf/tests/gateway/test_django.py diff --git a/pyamf/tests/gateway/test_google.py b/src/pyamf/tests/gateway/test_google.py similarity index 100% rename from pyamf/tests/gateway/test_google.py rename to src/pyamf/tests/gateway/test_google.py diff --git a/pyamf/tests/gateway/test_twisted.py b/src/pyamf/tests/gateway/test_twisted.py similarity index 100% rename from pyamf/tests/gateway/test_twisted.py rename to src/pyamf/tests/gateway/test_twisted.py diff --git a/pyamf/tests/gateway/test_wsgi.py b/src/pyamf/tests/gateway/test_wsgi.py similarity index 100% rename from pyamf/tests/gateway/test_wsgi.py rename to src/pyamf/tests/gateway/test_wsgi.py diff --git a/pyamf/tests/imports/spam.py b/src/pyamf/tests/imports/spam.py similarity index 100% rename from pyamf/tests/imports/spam.py rename to src/pyamf/tests/imports/spam.py diff --git a/pyamf/tests/modules/__init__.py b/src/pyamf/tests/modules/__init__.py similarity index 100% rename from pyamf/tests/modules/__init__.py rename to src/pyamf/tests/modules/__init__.py diff --git a/pyamf/tests/modules/test_decimal.py b/src/pyamf/tests/modules/test_decimal.py similarity index 100% rename from pyamf/tests/modules/test_decimal.py rename to src/pyamf/tests/modules/test_decimal.py diff --git a/pyamf/tests/modules/test_sets.py b/src/pyamf/tests/modules/test_sets.py similarity index 100% rename from pyamf/tests/modules/test_sets.py rename to src/pyamf/tests/modules/test_sets.py diff --git a/pyamf/tests/remoting/__init__.py b/src/pyamf/tests/remoting/__init__.py similarity index 100% rename from pyamf/tests/remoting/__init__.py rename to src/pyamf/tests/remoting/__init__.py diff --git a/pyamf/tests/remoting/test_amf0.py b/src/pyamf/tests/remoting/test_amf0.py similarity index 100% rename from pyamf/tests/remoting/test_amf0.py rename to src/pyamf/tests/remoting/test_amf0.py diff --git a/pyamf/tests/remoting/test_client.py b/src/pyamf/tests/remoting/test_client.py similarity index 100% rename from pyamf/tests/remoting/test_client.py rename to src/pyamf/tests/remoting/test_client.py diff --git a/pyamf/tests/remoting/test_remoteobject.py b/src/pyamf/tests/remoting/test_remoteobject.py similarity index 100% rename from pyamf/tests/remoting/test_remoteobject.py rename to src/pyamf/tests/remoting/test_remoteobject.py diff --git a/pyamf/tests/test_adapters.py b/src/pyamf/tests/test_adapters.py similarity index 100% rename from pyamf/tests/test_adapters.py rename to src/pyamf/tests/test_adapters.py diff --git a/pyamf/tests/test_adapters_util.py b/src/pyamf/tests/test_adapters_util.py similarity index 100% rename from pyamf/tests/test_adapters_util.py rename to src/pyamf/tests/test_adapters_util.py diff --git a/pyamf/tests/test_alias.py b/src/pyamf/tests/test_alias.py similarity index 100% rename from pyamf/tests/test_alias.py rename to src/pyamf/tests/test_alias.py diff --git a/pyamf/tests/test_amf0.py b/src/pyamf/tests/test_amf0.py similarity index 100% rename from pyamf/tests/test_amf0.py rename to src/pyamf/tests/test_amf0.py diff --git a/pyamf/tests/test_amf3.py b/src/pyamf/tests/test_amf3.py similarity index 100% rename from pyamf/tests/test_amf3.py rename to src/pyamf/tests/test_amf3.py diff --git a/pyamf/tests/test_basic.py b/src/pyamf/tests/test_basic.py similarity index 100% rename from pyamf/tests/test_basic.py rename to src/pyamf/tests/test_basic.py diff --git a/pyamf/tests/test_codec.py b/src/pyamf/tests/test_codec.py similarity index 100% rename from pyamf/tests/test_codec.py rename to src/pyamf/tests/test_codec.py diff --git a/pyamf/tests/test_flex.py b/src/pyamf/tests/test_flex.py similarity index 100% rename from pyamf/tests/test_flex.py rename to src/pyamf/tests/test_flex.py diff --git a/pyamf/tests/test_flex_messaging.py b/src/pyamf/tests/test_flex_messaging.py similarity index 100% rename from pyamf/tests/test_flex_messaging.py rename to src/pyamf/tests/test_flex_messaging.py diff --git a/pyamf/tests/test_gateway.py b/src/pyamf/tests/test_gateway.py similarity index 100% rename from pyamf/tests/test_gateway.py rename to src/pyamf/tests/test_gateway.py diff --git a/pyamf/tests/test_imports.py b/src/pyamf/tests/test_imports.py similarity index 100% rename from pyamf/tests/test_imports.py rename to src/pyamf/tests/test_imports.py diff --git a/pyamf/tests/test_remoting.py b/src/pyamf/tests/test_remoting.py similarity index 100% rename from pyamf/tests/test_remoting.py rename to src/pyamf/tests/test_remoting.py diff --git a/pyamf/tests/test_sol.py b/src/pyamf/tests/test_sol.py similarity index 100% rename from pyamf/tests/test_sol.py rename to src/pyamf/tests/test_sol.py diff --git a/pyamf/tests/test_util.py b/src/pyamf/tests/test_util.py similarity index 100% rename from pyamf/tests/test_util.py rename to src/pyamf/tests/test_util.py diff --git a/pyamf/tests/test_versions.py b/src/pyamf/tests/test_versions.py similarity index 100% rename from pyamf/tests/test_versions.py rename to src/pyamf/tests/test_versions.py diff --git a/pyamf/tests/test_xml.py b/src/pyamf/tests/test_xml.py similarity index 100% rename from pyamf/tests/test_xml.py rename to src/pyamf/tests/test_xml.py diff --git a/pyamf/tests/util.py b/src/pyamf/tests/util.py similarity index 100% rename from pyamf/tests/util.py rename to src/pyamf/tests/util.py diff --git a/pyamf/util/__init__.py b/src/pyamf/util/__init__.py similarity index 100% rename from pyamf/util/__init__.py rename to src/pyamf/util/__init__.py diff --git a/src/pyamf/util/imports.py b/src/pyamf/util/imports.py new file mode 100644 index 00000000..0d6e9137 --- /dev/null +++ b/src/pyamf/util/imports.py @@ -0,0 +1,281 @@ +# Copyright (c) The PyAMF Project. +# See LICENSE.txt for details. + +""" +Tools for doing dynamic imports. + +@since: 0.3 +""" +import importlib +import importlib.abc +import sys + +__all__ = ['when_imported'] + + +def when_imported(name, *hooks): + """ + Call C{hook(module)} when module named C{name} is first imported. C{name} + must be a fully qualified (i.e. absolute) module name. + + C{hook} must accept one argument: which will be the imported module object. + + If the module has already been imported, 'hook(module)' is called + immediately, and the module object is returned from this function. If the + module has not been imported, then the hook is called when the module is + first imported. + """ + global finder + + finder.when_imported(name, *hooks) + + +class ModuleLoader(importlib.abc.Loader): + """ + Loader wrapper that delegates actual loading to the original loader and then + runs post-import hooks. + """ + + def __init__(self, finder, name, loader): + self.finder = finder + self.name = name + self.loader = loader + + def create_module(self, spec): + if hasattr(self.loader, 'create_module'): + return self.loader.create_module(spec) + + return None + + def exec_module(self, module): + if hasattr(self.loader, 'exec_module'): + self.loader.exec_module(module) + else: + # Compatibility fallback for old loaders. + loaded = self.loader.load_module(self.name) + + if loaded is not module: + module.__dict__.update(loaded.__dict__) + + self.finder._run_hooks(self.name, module) + + +class ModuleFinder(importlib.abc.MetaPathFinder): + """ + This is a special module finder object that executes a collection of + callables when a specific module has been imported. An instance of this + is placed in C{sys.meta_path}, allowing us to provide this functionality. + + @ivar post_load_hooks: C{dict} of C{full module path -> callable} to be + executed when the module is imported. + @ivar loaded_modules: C{list} of modules that this finder has seen. Used + to stop recursive imports. + @see: L{when_imported} + @since: 0.5 + """ + + def __init__(self): + self.post_load_hooks = {} + self.loaded_modules = [] + + def find_spec(self, name, path=None, target=None): + """ + This is a new MoudleFinder implementation compatible with Py>=3.11 + + Called when an import is made. If there are hooks waiting for this + module to be imported then we wrap the normal loader and run hooks after + the module has been executed. + + @param name: The name of the module being imported. + @param path: The root path of the module, if importing from a package. + @param target: Existing module object during reloads. + @return: A ModuleSpec with a wrapped loader, or C{None} to allow the + standard import process to continue. + """ + if name in self.loaded_modules: + return None + + hooks = self.post_load_hooks.get(name, None) + + if not hooks: + return None + + self.loaded_modules.append(name) + + try: + spec = self._find_wrapped_spec(name, path, target) + except Exception: + self.loaded_modules.pop() + raise + + if spec is None or spec.loader is None: + self.loaded_modules.pop() + return None + + spec.loader = ModuleLoader(self, name, spec.loader) + + return spec + + def _find_wrapped_spec(self, name, path=None, target=None): + """ + Find the real module spec without invoking this finder recursively. + """ + meta_path = sys.meta_path[:] + + try: + sys.meta_path.remove(self) + except ValueError: + pass + + try: + return importlib.util.find_spec(name) + finally: + sys.meta_path[:] = meta_path + + def when_imported(self, name, *hooks): + """ + @see: L{when_imported} + """ + if name in sys.modules: + for hook in hooks: + hook(sys.modules[name]) + + return + + h = self.post_load_hooks.setdefault(name, []) + h.extend(hooks) + + def _run_hooks(self, name, module): + """ + Run all hooks for a module. + """ + try: + hooks = self.post_load_hooks.pop(name, []) + + for hook in hooks: + hook(module) + finally: + try: + self.loaded_modules.remove(name) + except ValueError: + pass + + def __getstate__(self): + return (self.post_load_hooks.copy(), self.loaded_modules[:]) + + def __setstate__(self, state): + self.post_load_hooks, self.loaded_modules = state + + + +class LegacyModuleFinder(object): + """ + This is original solution incompatible with Py>=3.11 + + This is a special module finder object that executes a collection of + callables when a specific module has been imported. An instance of this + is placed in C{sys.meta_path}, which is consulted before C{sys.modules} - + allowing us to provide this functionality. + + @ivar post_load_hooks: C{dict} of C{full module path -> callable} to be + executed when the module is imported. + @ivar loaded_modules: C{list} of modules that this finder has seen. Used + to stop recursive imports in L{load_module} + @see: L{when_imported} + @since: 0.5 + """ + + def __init__(self): + self.post_load_hooks = {} + self.loaded_modules = [] + + def find_module(self, name, path=None): + """ + Called when an import is made. If there are hooks waiting for this + module to be imported then we stop the normal import process and + manually load the module. + + @param name: The name of the module being imported. + @param path The root path of the module (if a package). We ignore this. + @return: If we want to hook this module, we return a C{loader} + interface (which is this instance again). If not we return C{None} + to allow the standard import process to continue. + """ + if name in self.loaded_modules: + return None + + hooks = self.post_load_hooks.get(name, None) + + if hooks: + return self + + def load_module(self, name): + """ + If we get this far, then there are hooks waiting to be called on + import of this module. We manually load the module and then run the + hooks. + + @param name: The name of the module to import. + """ + self.loaded_modules.append(name) + + try: + __import__(name, {}, {}, []) + + mod = sys.modules[name] + self._run_hooks(name, mod) + except: + self.loaded_modules.pop() + + raise + + return mod + + def when_imported(self, name, *hooks): + """ + @see: L{when_imported} + """ + if name in sys.modules: + for hook in hooks: + hook(sys.modules[name]) + + return + + h = self.post_load_hooks.setdefault(name, []) + h.extend(hooks) + + def _run_hooks(self, name, module): + """ + Run all hooks for a module. + """ + hooks = self.post_load_hooks.pop(name, []) + + for hook in hooks: + hook(module) + + def __getstate__(self): + return (self.post_load_hooks.copy(), self.loaded_modules[:]) + + def __setstate__(self, state): + self.post_load_hooks, self.loaded_modules = state + + + +def _init(): + """ + Internal function to install the module finder. + """ + global finder + + if finder is None: + if sys.version_info >= (3, 11): + finder = ModuleFinder() + else: + finder = LegacyModuleFinder() + + if finder not in sys.meta_path: + sys.meta_path.insert(0, finder) + + +finder = None +_init() diff --git a/pyamf/util/pure.py b/src/pyamf/util/pure.py similarity index 100% rename from pyamf/util/pure.py rename to src/pyamf/util/pure.py diff --git a/pyamf/versions.py b/src/pyamf/versions.py similarity index 100% rename from pyamf/versions.py rename to src/pyamf/versions.py diff --git a/pyamf/xml.py b/src/pyamf/xml.py similarity index 100% rename from pyamf/xml.py rename to src/pyamf/xml.py