From 0e18784456dc9cf510e1abdc15bb99ac2d280f33 Mon Sep 17 00:00:00 2001 From: brettlangdon Date: Sat, 25 Aug 2018 18:04:35 -0400 Subject: [PATCH] Finalize prototype --- .gitignore | 4 ++ CHANGELOG | 10 +++++ LICENSE | 21 +++++++++ MANIFEST.in | 1 + README.md | 94 +++++++++++++++++++++++++++++++++++++++++ setup.py | 37 ++++++++++++++++ virtualmod.py | 115 ++++++++++++++++++++++++-------------------------- 7 files changed, 222 insertions(+), 60 deletions(-) create mode 100644 .gitignore create mode 100644 CHANGELOG create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f430fcd --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.egg-info +dist +build +.py[co] diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..20e4a0c --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,10 @@ +virtualmod Changelog +=================== + +Version 1.0.0 +------------- + +:Released: 09-25-2018 + +:Changes: + Initial release diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0dd6d92 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Brett Langdon + +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. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..901cbf4 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include CHANGELOG LICENSE README.rst virtualmod.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..f08a878 --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +virtualmod +========== +Python package for creating and importing virtual modules. + +## Install + +```bash +pip install virtualmod +``` + +## Examples +### Module object +Manually creating and registering a module with `virtualmod`. + +```python +import virtualmod + +# Create a new empty virtual module +module = virtualmod.create_module('custom_module') + +# Add attribute to module +module.key = 'value' + + +# Use decorator to add a function to the module +# NOTE: You can use `add_to_module(module, name='new_name')` to override the module attribute name +@virtualmod.add_to_module(module) +def module_function(name): + print('Hello', name) + + +# Use decorator to add a class to the module +@virtualmod.add_to_module(module) +class ModuleClass: + pass + + +# Import and use our virtual module +import custom_module + +print('Key:', custom_module.key) +custom_module.module_function('virtualmod') +print(custom_module.ModuleClass()) +``` + +### Class definition +`virtualmod` also comes with the ability to use class definitions to define virtual modules. + +```python +import virtualmod + + +# Use class definition to define our virtual module "custom_module" +class CustomModule(virtualmod.VirtualModule): + # Define the module's name (would be "CustomModule" otherwise) + __module_name__ = 'custom_module' + + # Add an attribute + key = 'value' + + # Add a function + # NOTE: There is no `cls` or `self` + def module_function(name): + print('Hello', name) + + # Add a class to the module + class ModuleClass: + pass + + +# Import and use our virtual module +import custom_module + +print('Key:', custom_module.key) +custom_module.module_function('virtualmod') +print(custom_module.ModuleClass()) +``` + +### Override an existing module +`virtualmod`'s module finder is registered before the standard builtin finders. +This means if you register a module under a name of an existing module yours would be found and loaded first + +```python +import virtualmod + +# Create a virtual module under the name "socket" +my_socket = virtualmod.create_module('socket') + +# Import socket module +import socket + +# Test if the loaded socket module is the one we created +print(socket is my_socket) +``` diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..5a30065 --- /dev/null +++ b/setup.py @@ -0,0 +1,37 @@ +""" +virtualmod +========== +""" +from setuptools import setup + + +def get_long_description(): + with open('README.md') as f: + rv = f.read() + return rv + + +setup( + name='virtualmod', + version='1.0.0', + url='https://github.com/brettlangdon/virtualmod', + license='MIT', + author='Brett Langdon', + author_email='me@brett.is', + description='Python package for creating and importing virtual modules.', + long_description=get_long_description(), + long_description_content_type='text/markdown', + py_modules=['virtualmod'], + zip_safe=False, + include_package_data=True, + platforms='any', + install_requires=[], + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python', + ] +) diff --git a/virtualmod.py b/virtualmod.py index 19b1619..f26cb7f 100644 --- a/virtualmod.py +++ b/virtualmod.py @@ -2,81 +2,47 @@ import importlib from importlib.abc import Loader, MetaPathFinder import sys +__all__ = [ + 'MetaVirtualModule', + 'VirtualModule', + 'add_to_module', + 'create_module', +] + +# Our virtual module registry registry = dict() +# Built in module class module_cls = type(sys) +# Built in module spec class spec_cls = type(sys.__spec__) -class VirtualModule: - __slots__ = ['name', 'module', 'spec'] - - def __init__(self, name): - self.name = name - self.module = module_cls(name) - self.spec = spec_cls(name=name, loader=VirtualModuleLoader) - setattr(self.module, '__spec__', self.spec) - - def set_attribute(self, name, value): - setattr(self.module, name, value) - - def delete_attribute(self, name): - delattr(self.module, name) - - -def create_module(name): - module = VirtualModule(name) - registry[name] = module +def create_module(module_name): + """Function for create a new empty virtual module and register it""" + module = module_cls(module_name) + setattr(module, '__spec__', spec_cls(name=module_name, loader=VirtualModuleLoader)) + registry[module_name] = module return module -def copy_module(module): - name = module.__name__ - if hasattr(module, '__spec__'): - name = module.__spec__.name - - virt_mod = VirtualModule(name) - for key, value in module.__dict__.items(): - if key in ('__spec__', '__name__', '__loader__', '__package__'): - continue - virt_mod.set_attribute(key, value) - - registry[name] = virt_mod - importlib.reload(module) - return virt_mod - - -def as_module(cls_or_name): - if isinstance(cls_or_name, str): - cls = None - name = cls_or_name - elif isinstance(cls_or_name, type): - cls = cls_or_name - name = getattr(cls, '__module_name__', cls.__name__) - else: - raise ValueError('Expected as_module to be passed a string or a class type') - - def wrapper(cls): - module = create_module(name) - - for key, value in cls.__dict__.items(): - if key.startswith('__') and key.endswith('__'): - continue - - module.set_attribute(key, value) - return module - - if cls is None: - return wrapper - return wrapper(cls) +def add_to_module(module, name=None): + """Decorator to register a function or class to a module""" + def wrapper(value): + key = name or getattr(value, '__name__', None) + if key: + setattr(module, key, value) + return value + return wrapper class VirtualModuleLoader(Loader): + """Module loader class used for pulling virtual modules from our registry""" def create_module(spec): if spec.name not in registry: return None - return registry[spec.name].module + return registry[spec.name] def exec_module(module): module_name = module.__name__ @@ -87,10 +53,39 @@ class VirtualModuleLoader(Loader): class VirtualModuleFinder(MetaPathFinder): + """Module finder to register with sys.meta_path for finding module specs from our registry""" def find_spec(fullname, path, target=None): if fullname in registry: - return registry[fullname].spec + return registry[fullname].__spec__ return None +class MetaVirtualModule(type): + """Metaclass used for automatically creating and registering VirtualModule class definitions""" + def __init__(cls, name, bases, attrs): + # Initialize the class + super(MetaVirtualModule, cls).__init__(name, bases, attrs) + + # Do not register our base class + if name == 'VirtualModule': + return + + module_name = getattr(cls, '__module_name__', cls.__name__) or name + # DEV: `create_module` will registry this module for us + module = create_module(module_name) + + # Copy over class attributes + for key, value in attrs.items(): + if key in ('__name__', '__module_name__', '__module__', '__qualname__'): + continue + setattr(module, key, value) + + +class VirtualModule(metaclass=MetaVirtualModule): + """Base virtual module class for creating modules from class definitions""" + pass + + +# Push our virtual module finder at the beginning of the sys.meta_path +# DEV: Push in first so we always look for virtual modules first sys.meta_path.insert(0, VirtualModuleFinder)