Skip to content

Commit

Permalink
Merge pull request #13 from hiroki0525/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
hiroki0525 authored May 30, 2021
2 parents ab8e0b8 + 96b17c0 commit f0c1482
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 182 deletions.
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ repos:
rev: 3.9.2
hooks:
- id: flake8
# - repo: https://github.com/pre-commit/mirrors-mypy
# rev: v0.812
# hooks:
# - id: mypy
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.812
hooks:
- id: mypy
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,19 @@ About strict parameter, please see [here](#NOTE) .
#### load_classes
```
load_classes(
pkg_name: str,
excludes: Optional[Iterable[str]] = None,
src: str,
excludes: Iterable[str] = (),
recursive: bool = False,
) -> Tuple[Type]:
```
This method read the Python package and return the tuple of class objects.
This method read the Python package or module and return the tuple of class objects.

**NOTE**

From version 1.5.0, `pkg_name` parameter is duplicated.
It will be deleted soon.
`load_functions` is also same.

- Directory
```
pkg/
Expand Down Expand Up @@ -168,12 +175,12 @@ loader.load_classes("..otherpkg")
#### load_functions
```
load_functions(
pkg_name: str,
excludes: Optional[Iterable[str]] = None,
src: str,
excludes: Iterable[str] = (),
recursive: bool = False,
) -> Tuple[Callable]:
```
This method read the Python package and return the tuple of functions.
This method read the Python package or module and return the tuple of functions.
The usage is the same as `load_classes`.

##### NOTE
Expand Down
4 changes: 2 additions & 2 deletions autoload/_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ def load_type(self):

@abstractmethod
def predicate(self):
raise Exception("'predicate' function is not defined.")
raise Exception("'predicate' method is not defined.")

@abstractmethod
def draw_comparison(self, file: str):
raise Exception("'draw_comparison' function is not defined.")
raise Exception("'draw_comparison' method is not defined.")


class _ClassContext(Context):
Expand Down
4 changes: 4 additions & 0 deletions autoload/_globals.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from enum import Enum
from typing import Any, Callable, Type, TypeVar


class LoadType(Enum):
func = "function"
clazz = "class"


Class_Or_Func = TypeVar("Class_Or_Func", Type[Any], Callable)
162 changes: 162 additions & 0 deletions autoload/_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import inspect
from abc import ABC, abstractmethod
from dataclasses import dataclass
from importlib import import_module
from os import listdir
from os import path as os_path
from sys import path as sys_path
from typing import Iterable, List

from ._context import Context
from ._globals import Class_Or_Func
from .exception import LoaderStrictModeError


def _is_module(name: str) -> bool:
return name.endswith(".py")


def _exclude_py(path: str) -> str:
if _is_module(path):
return path.replace(".py", "")
return path


def _exclude_ex(file_name: str) -> str:
return os_path.splitext(file_name)[0]


_DECORATOR_ATTR = "_load_flg"


@dataclass(frozen=True)
class ImportOption:
recursive: bool = False
excludes: Iterable[str] = ()
is_strict: bool = False


class Importable(ABC):
def __init__(
self, path: str, context: Context, option: ImportOption = ImportOption()
):
self._path = _exclude_py(path)
self._context = context
self._option = option
children = self._load_children()
fix_excludes = [_exclude_py(e) for e in option.excludes]
self._children: List[Importable] = [
child for child in children if child.get_base_name() not in fix_excludes
]

@abstractmethod
def import_resources(self) -> List[Class_Or_Func]:
raise Exception("'import_resources' method is not defined.")

def get_base_name(self) -> str:
return _exclude_ex(os_path.basename(self._path))

def _load_children(self) -> List["Importable"]:
return []


class _Module(Importable):
def import_resources(self) -> List[Class_Or_Func]:
file = self.get_base_name()
context = self._context
is_strict = self._option.is_strict
load_type_name = context.load_type.value
module = import_module(file)
target_load_name = context.draw_comparison(file)
is_found = False
error = None
mods = []
members = inspect.getmembers(module, context.predicate())
for mod_name, mod in members:
is_name_match = target_load_name == mod_name
if hasattr(mod, _DECORATOR_ATTR):
if not getattr(mod, _DECORATOR_ATTR):
continue
if is_found:
# High priority error
error = LoaderStrictModeError(
f"Loader can only load a "
f"'{target_load_name}' {load_type_name} in {file} module."
f"\nPlease check '{mod_name}' in {file} module."
)
break
if is_strict and not is_name_match:
error = LoaderStrictModeError(
f"Loader can't load '{mod_name}' in {file} module."
f"\nPlease rename '{target_load_name}' {load_type_name}."
)
continue
mods.append(mod)
if is_strict:
if error:
# High priority error
error = LoaderStrictModeError(
f"Loader can only load a "
f"'{target_load_name}' {load_type_name} "
f"in {file} module."
)
break
is_found = True
continue
if not is_name_match:
continue
mods.append(mod)
if is_strict:
if error:
# High priority error
error = LoaderStrictModeError(
f"Loader can only load a "
f"'{target_load_name}' {load_type_name} in {file} module."
)
break
is_found = True
if error is not None:
raise error
return mods


class _Package(Importable):
def import_resources(self) -> List[Class_Or_Func]:
return [
resource
for child in self._children
for resource in child.import_resources()
]

def _load_children(self) -> List[Importable]:
path = self._path
option = self._option
children = []
for file_or_dir in listdir(path):
if file_or_dir in option.excludes:
continue
is_module = _is_module(file_or_dir)
is_pkg = os_path.isdir(f"{path}/{file_or_dir}")
if not is_module and not is_pkg:
continue
if is_pkg and not option.recursive:
continue
fixed_file_or_dir = _exclude_ex(file_or_dir) if is_module else file_or_dir
children.append(
ImportableFactory.get(
f"{path}/{fixed_file_or_dir}", self._context, option
)
)
return children


class ImportableFactory:
@staticmethod
def get(path: str, *args, **kwargs) -> Importable:
is_dir = os_path.isdir(path)
fixed_path = path if is_dir else "/".join(path.split("/")[:-1])
if fixed_path not in sys_path:
sys_path.append(fixed_path)
if is_dir:
return _Package(path, *args, **kwargs)
return _Module(path, *args, **kwargs)
10 changes: 7 additions & 3 deletions autoload/decorator.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
__all__ = "load_config"

from typing import Optional
from typing import Callable, Optional

from autoload._globals import Class_Or_Func

def load_config(order: Optional[int] = None, load: bool = True):
def decorator(resource):

def load_config(
order: Optional[int] = None, load: bool = True
) -> Callable[[Class_Or_Func], Class_Or_Func]:
def decorator(resource: Class_Or_Func):
if order:
setattr(resource, "_load_order", order)
setattr(resource, "_load_flg", load)
Expand Down
Loading

0 comments on commit f0c1482

Please sign in to comment.