# coding=utf-8
import fnmatch
from itertools import groupby
from operator import attrgetter
from typing import Dict, List
from abc import ABCMeta, abstractmethod
_sorter = attrgetter('module', 'package', 'name')
"""used to sort classes or depdencies by module, package and name"""
[docs]class ClassInfo(object):
"""Information of a class."""
def __init__(self,
path: str,
name: str,
package: str,
module: str,
layer: str):
""""Initialize class information.
Args:
layer: layer which the class belongs to
module: module which the class belongs to
package: package of the class
name: name of the class
path: file path of the class file
"""
self.layer = layer
self.path = path
self.name = name
self.package = package
self.module = module
@property
def full_name(self) -> str:
"""includes both package and name"""
return "{}.{}".format(self.package, self.name)
@property
def is_production(self) -> bool:
"""True if the class is production code"""
return self.layer is not None and self.module is not None and self.package is not None
[docs] def is_layer(self, layer) -> bool:
"""Check whether class belongs to specfic layer.
Args:
layer: layer's name
Returns:
True if class belongs to layer
"""
return self.layer == layer
[docs] def oneline_str(self, template) -> str:
return template.format(**self.__all_attributes) if self.is_production else self.full_name
@property
def __all_attributes(self) -> Dict[str, str]:
all_attrs = dict(vars(self), full_name=self.full_name)
all_attrs['class'] = all_attrs['name']
return all_attrs
@property
def hierarchy_path(self) -> Dict[str, str]:
return {'layer': self.layer,
'module': self.module,
'package': self.package,
'class': self.name}
[docs] def path_match(self, ignore_none=False, **kwargs) -> bool:
hierarchy_path = self.hierarchy_path
path_pattern_dict = path_to_wd_dict(kwargs)
for path_key, pattern in path_pattern_dict.items():
if not hierarchy_path.get(path_key) and ignore_none:
continue
if not fnmatch.fnmatch(hierarchy_path.get(path_key), pattern):
return False
return True
def __str__(self):
return str(self.__dict__)
def __eq__(self, other):
if isinstance(other, ClassInfo):
return self.path == other.path
return False
def __ne__(self, other):
return (not self.__eq__(other))
def __hash__(self):
return hash(self.path)
[docs]class Dependency(ClassInfo):
def __init__(self,
path: str,
name: str,
package: str,
module: str,
layer: str):
super().__init__(path, name, package, module, layer)
self.bad_smells = []
def __eq__(self, other):
if isinstance(other, Dependency):
return super().__eq__(other)
return False
@property
def has_smell(self):
return len(self.bad_smells) > 0
[docs] def oneline_str(self, template):
_str = super().oneline_str(template)
if self.has_smell:
_str += " (" + "; ".join(
[bs.description for bs in self.bad_smells]) + ") "
return _str
def __hash__(self):
return hash(self.path)
[docs]class Component(metaclass=ABCMeta):
"""A component is something that has dependencies and usages, such as class,
package, module and layer.
"""
@property
@abstractmethod
def dependencies(self) -> List[Dependency]:
"""all dependencies of the component in order."""
pass
@property
@abstractmethod
def usages(self) -> List[Dependency]:
"""all usages of the component in order."""
pass
@property
def smell_dependencies(self) -> List[Dependency]:
"""all smell dependencies of component in order."""
return [d for d in self.dependencies if d.has_smell]
@property
def smell_usages(self) -> List[Dependency]:
"""all smell usages of the component in order."""
return [u for u in self.usages if u.has_smell]
@property
@abstractmethod
def hierarchy_path(self) -> Dict[str, str]:
"""path of the component in the hierarchy"""
pass
[docs]class Class(ClassInfo, Component):
def __init__(self,
path,
name,
package,
module,
layer=None,
dependencies=[]):
self.dependencies = dependencies
self.usages = []
super().__init__(path, name, package, module, layer)
@property
def dependencies(self) -> List[Dependency]:
return self._dependencies
@dependencies.setter
def dependencies(self, dependencies):
self._dependencies = sorted(dependencies, key=_sorter)
@property
def usages(self):
return self._usages
@usages.setter
def usages(self, usages):
self._usages = sorted(usages, key=_sorter)
[docs]class ComponentList(Component, metaclass=ABCMeta):
@property
@abstractmethod
def item_type(self):
pass
@property
def dependencies(self):
return sorted([d for c in self.classes for d in c.dependencies], key=_sorter)
@property
def usages(self):
return sorted([u for c in self.classes for u in c.usages], key=_sorter)
def __init__(self, name, parent=None) -> None:
super().__init__()
self.name = name
self.parent = parent
@property
def items(self) -> List[Component]:
return self._items
@items.setter
def items(self, items: List[Component]):
self._items = sorted(items, key=attrgetter("name"))
def __getitem__(self, key: str) -> Component:
return next((item for item in self._items if item.name == key), None)
def __setitem__(self, key: str, component: Component):
found = self.__getitem__(key)
if not found:
self._items.append(component)
self._items = sorted(self._items, key=attrgetter("name"))
def __delitem__(self, key):
found = self.__getitem__(key)
if found:
self._items.remove(found)
[docs] def find_items(self, wildcards):
return self.separate_items(wildcards)[0]
[docs] def separate_items(self, wildcards):
if not wildcards:
return list(self.items), []
match_items, unmatch_items = [], []
for item in self.items:
match_items.append(item) if fnmatch.fnmatch(
item.name, wildcards) else unmatch_items.append(item)
return match_items, unmatch_items
@property
def classes(self):
return [cls for item in self.items for cls in item.classes]
@property
def smell_dependency_classes(self):
return [c for c in self.classes if len(c.smell_dependencies) > 0]
@property
def smell_uasge_classes(self):
return [c for c in self.classes if len(c.smell_usages) > 0]
[docs]class Package(ComponentList):
def __init__(self, name, classes=list(), module=None, layer=None, **_):
self.classes = classes
super().__init__(name, parent=module)
self.layer = layer
@property
def item_type(self):
return Class
@property
def module(self):
return self.parent
@property
def package(self):
return self.name
@property
def classes(self) -> List[Class]:
return self.items
@classes.setter
def classes(self, classes):
self.items = sorted(classes, key=_sorter)
[docs] def oneline_str(self, template):
return template.format(**self.all_attributes)
@property
def all_attributes(self):
return dict(vars(self), full_name=self.name, module=self.module)
@property
def hierarchy_path(self):
return {'layer': self.layer, 'module': self.module, 'package': self.package}
[docs]class Module(ComponentList):
def __init__(self, name: str, packages: List[Package] = list(), layer: str = None, **_):
self.packages = packages
super().__init__(name, parent=layer)
@property
def item_type(self):
return Package
@property
def module(self):
return self.name
@property
def layer(self):
return self.parent
@property
def packages(self) -> List[Package]:
return self.items
@packages.setter
def packages(self, packages: List[Package]):
self.items = packages
@property
def hierarchy_path(self):
return {'layer': self.layer, 'module': self.module, }
[docs]class Layer(ComponentList):
def __init__(self, name: str, modules: List[Module] = list(), **kwargs):
self.modules = modules
super().__init__(name)
@property
def item_type(self):
return Module
@property
def modules(self) -> List[Module]:
return self.items
@modules.setter
def modules(self, modules: List[Module]):
self.items = modules
@property
def packages(self) -> List[Package]:
return [p for m in self.modules for p in m.pacakges]
@property
def layer(self):
return self.name
@property
def hierarchy_path(self):
return {'layer': self.layer}
[docs]class Hierarchy(ComponentList):
def __init__(self, layers: List[Layer] = list(), **_):
super().__init__('Hierarchy')
self.items = layers
@property
def item_type(self):
return Layer
@property
def layers(self) -> List[Layer]:
return self.items
@layers.setter
def layers(self, layers: List[Layer]):
self.items = layers
@property
def modules(self):
return [module for layer in self.layers for module in layer.modules]
@property
def packages(self) -> List[Package]:
return [packge for module in self.modules for packge in module.pacakges]
@property
def hierarchy_path(self):
return None
[docs]def path_to_wd_dict(path_or_path_dict: Dict[str, str] or str) -> Dict[str, str]:
full_path = path_or_path_dict if type(
path_or_path_dict) is str else path_or_path_dict.get('path', None)
if full_path is not None:
path_or_path_dict = dict(
zip(['layer', 'module', 'package', 'class'], full_path.split(':')))
return path_or_path_dict
[docs]def wd_dict_to_path(from_dict):
return "{}{}{}{}".format(
"" +
from_dict["layer"] if "layer" in from_dict.keys() else "",
":" +
from_dict["module"] if "module" in from_dict.keys() else "",
":" +
from_dict["package"] if "package" in from_dict.keys() else "",
":" +
from_dict["class"] if "class" in from_dict.keys() else "")
[docs]def group_class_by_module_package(classes: List[Class]) -> Dict[str, Dict[str, Class]]:
"""Group class by its attributes: 'module', 'pacakge' and 'name', returns an embeded dict"""
module_dict = {}
sorted_classes = sorted(classes, key=_sorter)
for m, m_cls_list in groupby(sorted_classes, key=attrgetter("module")):
package_dict = {}
for p, p_cls_list in groupby(list(m_cls_list), key=attrgetter("package")):
package_dict[p] = list(p_cls_list)
module_dict[m] = package_dict
return module_dict