Skip to content

Commit

Permalink
Merge pull request #2 from hiroki0525/feature/function-load
Browse files Browse the repository at this point in the history
Feature/function load
  • Loading branch information
hiroki0525 authored Nov 3, 2020
2 parents ede53a4 + ff5cba9 commit 034a0af
Show file tree
Hide file tree
Showing 57 changed files with 438 additions and 191 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,4 @@ dmypy.json

# Pyre type checker
.pyre/
/.MY_README.md
7 changes: 7 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM python:3.8-alpine
RUN pip install -U pip \
&& pip install -i https://test.pypi.org/simple/ autoload-module
COPY tests/main.py .
COPY tests/ tests
RUN rm tests/main.py
ENTRYPOINT ["python", "main.py"]
23 changes: 20 additions & 3 deletions JP_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,21 @@ loader.load_classes("..packageA.validator")
loader.load_classes("../packageA/validator")
```

#### load_functions
```
load_functions(pkg_name, [excludes])
```
引数で与えられたパッケージ名から配下のモジュールをimportし、関数オブジェクトのタプルを返却します。
使い方は `load_classes` と同じです。

**NOTE**
- クラスを検索するために, **モジュール名とクラス名を一致させてください.**
例えば, もし `test_module.py` と命名したのであれば, クラス名は `TestModule` にしてください。
クラス名をカスタマイズしたい場合は, `@load_config` デコレータで `load=True` を指定してください。
- クラスや関数を検索するために, **モジュール名とクラス名また関数名を一致させてください.**
例えば, もし `test_module.py` と命名したのであれば, クラス名は `TestModule` 、関数名は `test_module` にしてください。
クラス名や関数名をカスタマイズしたい場合は, `@load_config` デコレータで `load=True` を指定してください。
- validator_a.py
```python
from autoload.decorator import load_config

@load_config(load=True)
class CustomValidator:
def validate(self):
Expand All @@ -130,6 +139,8 @@ loader.load_classes("../packageA/validator")
- 返却されるクラスオブジェクトに順番を持たせたいなら、同じく `@load_config` デコレータを使ってください。
- validator_a.py
```python
from autoload.decorator import load_config

# 昇順でソートされます
@load_config(order=1)
class ValidatorA:
Expand Down Expand Up @@ -162,5 +173,11 @@ clazz().validate()
```
`file_name`の指定方法は `load_classes` と同じです。

```
load_function(file_name)
```
Pythonファイルをimportして関数オブジェクトを返却します。
使い方は `load_class` と同じです。

## License
Released under the MIT license.
11 changes: 0 additions & 11 deletions Pipfile

This file was deleted.

20 changes: 0 additions & 20 deletions Pipfile.lock

This file was deleted.

26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ except:
```
## Install
```
pip install autoload_module
pip install autoload-module
```
## Usage
### Constructor
Expand Down Expand Up @@ -118,12 +118,21 @@ loader.load_classes("..packageA.validator")
loader.load_classes("../packageA/validator")
```

#### load_functions
```
load_functions(pkg_name, [excludes])
```
This method read the Python package and return the tuple of functions.
The usage is the same as `load_classes`.

**NOTE**
- To search class, **You must match the file name and class name.**
For example, if you named the file `test_module.py`, you must named the class `TestModule`.
When you want to customize the class name, use `@load_config` decorator and write `load=True` manually.
- To search class or function, **You must match the name of file and the one of class or function.**
For example, if you named the file `test_module.py`, you must named the class `TestModule` or the function `test_module`.
When you want to customize their name, use `@load_config` decorator and write `load=True` manually.
- validator_a.py
```python
from autoload.decorator import load_config

@load_config(load=True)
class CustomValidator:
def validate(self):
Expand All @@ -132,6 +141,8 @@ When you want to customize the class name, use `@load_config` decorator and writ
- You can also control the order of loaded class objects using `@load_config` decorator.
- validator_a.py
```python
from autoload.decorator import load_config

# sort in ascending order
@load_config(order=1)
class ValidatorA:
Expand Down Expand Up @@ -164,5 +175,12 @@ clazz().validate()
```
How to specify `file_name` is the same as that of `load_classes`.

#### load_function
```
load_class(file_name)
```
This method read the Python file and return the function object.
The usage is the same as `load_function`.

## License
Released under the MIT license.
8 changes: 4 additions & 4 deletions autoload/decorator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
def load_config(order=None, load=False):
def decorator(cls):
def decorator(resource):
if order:
cls.load_order = order
cls.load_flg = load
return cls
resource.load_order = order
resource.load_flg = load
return resource
return decorator
172 changes: 109 additions & 63 deletions autoload/module_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,84 +3,52 @@
import os
import sys

__all__ = (
"ModuleLoader"
)

OP = os.path
SP = sys.path
THIS_FILE = OP.basename(__file__)
DEFAULT_EXCLUDES = (
'__init__.py',
THIS_FILE,
)
DECORATOR_ATTR = "load_flg"

class ModuleLoader:
op = os.path
sp = sys.path
this_file = op.basename(__file__)
default_excludes = (
'__init__.py',
this_file,
)

class ModuleLoader:
def __init__(self, base_path=None):
self.__base_path = self.__init_base_url(base_path)
self.__context = None

def load_class(self, file_name):
target_file = file_name
if file_name.endswith('.py'):
target_file = file_name.replace('.py', '')
fix_path_arr = self.__path_fix(target_file).split('/')
target_file = fix_path_arr[-2]
target_path = '/'.join(fix_path_arr[:-2])
if target_path not in self.sp:
self.sp.append(target_path)
module = importlib.import_module(target_file)
for mod_name, clazz in inspect.getmembers(module, inspect.isclass):
if hasattr(clazz, "load_flg") and clazz.load_flg:
return clazz
if "".join(target_file.split("_")).lower() != mod_name.lower():
continue
return clazz
self.__context = self.Context(self.Context.Type.clazz)
return self.__load_resource(file_name)

def load_classes(self, pkg_name=None, excludes=None):
target_dir = self.__path_fix(pkg_name)
if not self.op.isdir(target_dir):
raise NotADirectoryError('Not Found The Directory : {}'.format(target_dir))
if target_dir not in self.sp:
self.sp.append(target_dir)
files = [self.op.splitext(file)[0] for file in os.listdir(target_dir) if file.endswith('.py')]
exclude_files = list(self.default_excludes)
exclude_files.append(self.op.basename(self.__detect_call_path()))
if excludes:
if not iter(excludes):
raise TypeError('excludes variable must be iterable.')
for exclude in excludes:
if not isinstance(exclude, str):
raise TypeError('The contents of the excludes must all be strings')
exclude_files.append(exclude)
fix_excludes = [exclude.replace('.py', '') for exclude in exclude_files]
excluded_files = tuple(set(files) - set(fix_excludes))
classes = []
for file in excluded_files:
module = importlib.import_module(file)
for mod_name, clazz in inspect.getmembers(module, inspect.isclass):
if hasattr(clazz, "load_flg") and clazz.load_flg:
classes.append(clazz)
break
if "".join(file.split("_")).lower() != mod_name.lower():
continue
classes.append(clazz)
has_order_classes = [clazz for clazz in classes if hasattr(clazz, 'load_order') and clazz.load_order]
if not has_order_classes:
return tuple(classes)
no_has_order_classes = [clazz for clazz in classes if not hasattr(clazz, 'load_order') or not clazz.load_order]
if not no_has_order_classes:
return tuple(sorted(has_order_classes, key=lambda clazz:clazz.load_order))
ordered_classes = sorted(has_order_classes, key=lambda clazz:clazz.load_order) + no_has_order_classes
return tuple(ordered_classes)
def load_function(self, file_name):
self.__context = self.Context(self.Context.Type.func)
return self.__load_resource(file_name)

def load_classes(self, pkg_name, excludes=None):
self.__context = self.Context(self.Context.Type.clazz)
return self.__load_resources(pkg_name, excludes=excludes)

def load_functions(self, pkg_name, excludes=None):
self.__context = self.Context(self.Context.Type.func)
return self.__load_resources(pkg_name, excludes=excludes, type='function')

def __detect_call_path(self):
for path in inspect.stack():
path_name = path.filename
filename = self.op.basename(path.filename)
if self.this_file == filename:
filename = OP.basename(path.filename)
if THIS_FILE == filename:
continue
return path_name

def __init_base_url(self, base_path=None):
if not base_path:
return self.op.dirname(self.__detect_call_path())
return OP.dirname(self.__detect_call_path())
if self.__base_path.endswith('/'):
return self.__base_path[:-1]
return base_path
Expand Down Expand Up @@ -128,4 +96,82 @@ def __path_fix(self, name):
return result_base_path + '/' + path + '/'
# example: foo.bar
path = '/'.join(name.split('.'))
return self.__base_path + '/' + path + '/'
return self.__base_path + '/' + path + '/'

def __load_resource(self, file_name):
target_file = file_name.replace('.py', '') if file_name.endswith('.py') else file_name
fix_path_arr = self.__path_fix(target_file).split('/')
target_file = fix_path_arr[-2]
target_path = '/'.join(fix_path_arr[:-2])
if target_path not in SP:
SP.append(target_path)
module = importlib.import_module(target_file)
comparison = self.__context.draw_comparison(target_file)
for mod_name, resource in inspect.getmembers(module, self.__context.predicate):
if hasattr(resource, DECORATOR_ATTR) and resource.load_flg:
return resource
if comparison != mod_name.lower():
continue
del self.__context
return resource

def __load_resources(self, pkg_name, excludes=None, type='class'):
target_dir = self.__path_fix(pkg_name)
if not OP.isdir(target_dir):
raise NotADirectoryError('Not Found The Directory : {}'.format(target_dir))
if target_dir not in SP:
SP.append(target_dir)
files = [OP.splitext(file)[0] for file in os.listdir(target_dir) if file.endswith('.py')]
exclude_files = list(DEFAULT_EXCLUDES)
exclude_files.append(OP.basename(self.__detect_call_path()))
if excludes:
if not iter(excludes):
raise TypeError('excludes variable must be iterable.')
for exclude in excludes:
if not isinstance(exclude, str):
raise TypeError('The contents of the excludes must all be strings')
exclude_files.append(exclude)
fix_excludes = [exclude.replace('.py', '') for exclude in exclude_files]
excluded_files = tuple(set(files) - set(fix_excludes))
classes = []
for file in excluded_files:
module = importlib.import_module(file)
for mod_name, clazz in inspect.getmembers(module, self.__context.predicate):
if hasattr(clazz, DECORATOR_ATTR) and clazz.load_flg:
classes.append(clazz)
break
if self.__context.draw_comparison(file) != mod_name.lower():
continue
classes.append(clazz)
del self.__context
has_order_classes = [clazz for clazz in classes if hasattr(clazz, 'load_order') and clazz.load_order]
if not has_order_classes:
return tuple(classes)
no_has_order_classes = [clazz for clazz in classes if not hasattr(clazz, 'load_order') or not clazz.load_order]
if not no_has_order_classes:
return tuple(sorted(has_order_classes, key=lambda clazz: clazz.load_order))
ordered_classes = sorted(has_order_classes, key=lambda clazz: clazz.load_order) + no_has_order_classes
return tuple(ordered_classes)

class Context:
# Don't use enum because it is not supported under Python 3.4 version
class Type:
func = 'function'
clazz = 'class'

def __init__(self, type):
self.__type = type
if type == self.Type.clazz:
self.__predicate = inspect.isclass
else:
self.__predicate = inspect.isfunction

@property
def predicate(self):
return self.__predicate

def draw_comparison(self, file):
if self.__type == self.Type.clazz:
return "".join(file.split("_")).lower()
else:
return file.lower()
35 changes: 35 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[tool.poetry]
name = "autoload-module"
version = "1.1.0"
description = "Python Autoload Module"
authors = ["Hiroki Miyaji <[email protected]>"]
license = "MIT"
maintainers = ["Hiroki Miyaji <[email protected]>"]
readme = "README.md"
homepage = "https://github.com/hiroki0525/autoload_module"
repository = "https://github.com/hiroki0525/autoload_module"
documentation = "https://github.com/hiroki0525/autoload_module"
keywords = ["python", "import", "autoload", "autoload_module", "metaprogramming"]
classifiers = [
'Topic :: Software Development :: Libraries',
'Development Status :: 1 - Planning',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
]
packages = [
{ include = "autoload" },
]

[tool.poetry.dependencies]
python = "^3.8"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
Loading

0 comments on commit 034a0af

Please sign in to comment.