Compare commits

..

23 Commits

Author SHA1 Message Date
cea3e35800 Smaller bugfixes 2022-01-19 22:15:14 +01:00
60e09252c8 Smaller bugfixes 2022-01-19 22:03:37 +01:00
b3e0083633 Added cpl commands to compile 2022-01-19 16:43:03 +01:00
fa5157298c Improved project for compiling 2022-01-19 16:31:31 +01:00
00484711ab Changed exit to sys.exit 2022-01-19 12:43:59 +01:00
83f666353f Improved sort criteria 2022-01-18 21:54:55 +01:00
405dded626 Minor bugfixes 2022-01-18 21:13:56 +01:00
cc56bf33ce Removed not existing attribute 2022-01-18 20:10:02 +01:00
2790dbfbab Fixed class movement in draw process 2022-01-18 20:02:39 +01:00
e5b3e80378 Fixed handling of multiple base classes 2022-01-18 19:19:01 +01:00
f41e31371f Added support for multiple subclasses 2022-01-18 19:15:48 +01:00
1eb7aa37d3 Added support for multiple base classes 2022-01-18 19:02:25 +01:00
5aad932c7c Minor bugfixes to handle attributes 2022-01-18 18:13:43 +01:00
f7b88fe127 Added logic to draw on x to given max point 2022-01-18 17:54:58 +01:00
318a8362ca Added logic to handle not external base classes 2022-01-18 17:00:53 +01:00
aa56fa473b Removed unused import 2022-01-16 20:11:47 +01:00
5ba24cafaf Added rendering of implementations 2022-01-16 20:11:13 +01:00
c53b3b7669 [WIP] Added implementation scanning 2022-01-16 16:10:20 +01:00
61b1838c40 Added width and height rendering 2022-01-16 14:56:02 +01:00
4b7f0e8231 Added umlet output 2022-01-16 13:45:01 +01:00
9758f9aa50 Removed unused import 2022-01-16 11:42:29 +01:00
f7edc44154 Fixed attribute parsing 2022-01-16 03:18:56 +01:00
9a4de29af8 Improved output 2022-01-16 02:35:51 +01:00
27 changed files with 797 additions and 95 deletions

View File

@@ -6,8 +6,15 @@
"py_to_uxf_core": "src/py_to_uxf_core/py_to_uxf_core.json" "py_to_uxf_core": "src/py_to_uxf_core/py_to_uxf_core.json"
}, },
"Scripts": { "Scripts": {
"build-start": "cpl build; cd dist/py_to_uxf/build/py_to_uxf; echo \"Starting:\"; bash py_to_uxf ./", "build-start": "cpl build; cd dist/py_to_uxf/build/py_to_uxf; echo \"Starting:\"; bash py_to_uxf -p ./ -o uml.uxf",
"bs": "cpl build-start" "build-start-test": "cpl build; cd dist/py_to_uxf/build/py_to_uxf; echo \"Starting:\"; bash py_to_uxf -p ../../../../sh_gismo/src -o gismo.uxf",
"compile": "cd src/; pyinstaller --specpath ../dist/cspec --workpath ../dist/cbuild --distpath ../dist/cdist --add-data ../../src/py_to_uxf/appsettings.json:. --hidden-import pynput.keyboard._xorg --hidden-import pynput.mouse._xorg --hidden-import pyfiglet.fonts --collect-data pyfiglet -n py_to_uxf -y py_to_uxf/main.py; cd ../;",
"compile-start": "cpl compile; cd dist/cdist/py_to_uxf; echo \"Starting:\"; ./py_to_uxf -p ../../../src -o uml.uxf; cd ../../../",
"compile-start-test": "cpl compile; cd dist/cdist/py_to_uxf; echo \"Starting:\"; ./py_to_uxf -p ../../../../sh_gismo/src -o gismo.uxf; cd ../../../",
"bs": "cpl build-start",
"bst": "cpl build-start-test",
"cs": "cpl compile-start",
"cst": "cpl compile-start-test"
} }
} }
} }

35
scripts/compile.sh Normal file
View File

@@ -0,0 +1,35 @@
#!/bin/bash
cd src
#pyinstaller --specpath ../dist/cspec \
# --workpath ../dist/cbuild \
# --distpath ../dist/cdist \
# --add-data ../../src/py_to_uxf/appsettings.json:. \
# --hidden-import pynput.keyboard._xorg \
# --hidden-import pynput.mouse._xorg \
# --hidden-import pyfiglet.fonts \
# --collect-data pyfiglet \
# -n py_to_uxf \
# -y \
# py_to_uxf/main.py
build=../dist/cbuild/
python -m nuitka \
--follow-imports \
--include-package=pkg_resources \
--include-package=pynput.keyboard._xorg \
--include-package=pynput.mouse._xorg \
--include-package=Xlib \
--output-dir=$build \
py_to_uxf/main.py
file="py_to_uxf/appsettings.json"
if [ -f "$file" ]; then
if [ -d "$build/main.dist/" ]; then
build=$build/main.dist/
fi
cp $file $build
fi
cd ../

5
scripts/run.sh Normal file
View File

@@ -0,0 +1,5 @@
cd dist/cdist/py_to_uxf
time ./py_to_uxf -p ../../../../sh_gismo/src -o gismo.uxf
cd ../../../

View File

@@ -1,9 +1,16 @@
import sys
import traceback
from cpl_core.application import ApplicationABC from cpl_core.application import ApplicationABC
from cpl_core.configuration import ConfigurationABC from cpl_core.configuration import ConfigurationABC
from cpl_core.console import Console from cpl_core.console import Console
from cpl_core.dependency_injection import ServiceProviderABC from cpl_core.dependency_injection import ServiceProviderABC
from cpl_query.extension import List
from py_to_uxf_core.abc.python_parser_abc import PythonParserABC from py_to_uxf_core.abc.python_parser_abc import PythonParserABC
from py_to_uxf_core.abc.umlet_creator_abc import UmletCreatorABC
from py_to_uxf_core.model.class_implementation import ClassImplementation
from py_to_uxf_core.model.python_class import PythonClass
class Application(ApplicationABC): class Application(ApplicationABC):
@@ -11,17 +18,69 @@ class Application(ApplicationABC):
def __init__(self, config: ConfigurationABC, services: ServiceProviderABC): def __init__(self, config: ConfigurationABC, services: ServiceProviderABC):
ApplicationABC.__init__(self, config, services) ApplicationABC.__init__(self, config, services)
self._path = config.get_configuration('path') self._path = config.get_configuration('p')
self._parser = services.get_service(PythonParserABC) self._file = ''
try:
self._file = config.get_configuration('-pAdditionalArguments')[1]
except Exception as e:
Console.error('Expected file')
sys.exit()
self._parser: PythonParserABC = services.get_service(PythonParserABC)
self._umlet_creator: UmletCreatorABC = services.get_service(UmletCreatorABC)
def configure(self): def configure(self):
pass pass
@staticmethod
def _console_output(classes: list[PythonClass]):
for cls in classes:
Console.write_line(f'Class {cls.name}')
if len(cls.attributes) > 0:
Console.write_line('\tAttributes:')
for atr in cls.attributes:
Console.write_line(f'\t{atr.access_modifier.value}{atr.name}: {atr.type}')
if len(cls.functions) > 0:
Console.write_line('\tFunctions:')
for func in cls.functions:
args = ''
Console.write_line(f'\t{func.access_modifier.value}{func.name}({args}): {func.return_type}')
Console.write_line()
@staticmethod
def _exit(msg: str, tb=None):
if tb is not None:
Console.error(msg, tb)
else:
Console.error(msg)
sys.exit()
def main(self): def main(self):
Console.banner('Py to UXF') Console.banner('Py to UXF')
if self._path is None: if self._path is None:
Console.write_line('Expected path') self._exit('Expected path\n')
return
Console.write_line(f'Found path:', self._path) if self._file is None:
self._parser.parse() self._exit(f'Expected output file\n')
Console.write_line(f'Input path:', self._path)
Console.write_line(f'Output path:', self._file)
xml = ''
try:
classes: List[PythonClass] = self._parser.parse_classes()
Console.write_line(f'Found {classes.count()} classes')
implementations: List[ClassImplementation] = self._parser.parse_implementations(classes)
Console.write_line(f'Found {implementations.count()} implementations')
# self._console_output(classes)
xml = self._umlet_creator.generate_xml(classes, implementations)
except Exception as e:
self._exit('Parsing failed', f'{e} -> {traceback.format_exc()}\n')
if not self._file.endswith('.uxf'):
self._exit(f'Unexpected file {self._file}\n')
with open(self._file, 'w+') as file:
file.write(xml)
file.close()
Console.write_line('\n')

View File

@@ -0,0 +1,11 @@
{
"Parser": {
"IgnoreClassNames": [
"ABC",
"Enum"
]
},
"UMLCreator": {
"MaxPixelX": 2000
}
}

View File

@@ -7,5 +7,5 @@ cd "$path/../"
export PYTHONPATH=./:$PYTHONPATH export PYTHONPATH=./:$PYTHONPATH
python3.9 py_to_uxf/main.py --path $@ python3.9 py_to_uxf/main.py $@
echo "" echo ""

View File

@@ -1,7 +1,9 @@
import os import os
import sys
from cpl_core.application import StartupABC from cpl_core.application import StartupABC
from cpl_core.configuration import ConfigurationABC, ConsoleArgument from cpl_core.configuration import ConfigurationABC, ConsoleArgument
from cpl_core.console import Console
from cpl_core.dependency_injection import ServiceProviderABC, ServiceCollectionABC from cpl_core.dependency_injection import ServiceProviderABC, ServiceCollectionABC
from cpl_core.environment import ApplicationEnvironment from cpl_core.environment import ApplicationEnvironment
@@ -9,12 +11,14 @@ from py_to_uxf_core.abc.attribute_scanner_abc import AttributeScannerABC
from py_to_uxf_core.abc.class_scanner_abc import ClassScannerABC from py_to_uxf_core.abc.class_scanner_abc import ClassScannerABC
from py_to_uxf_core.abc.file_scanner_abc import FileScannerABC from py_to_uxf_core.abc.file_scanner_abc import FileScannerABC
from py_to_uxf_core.abc.function_scanner_abc import FunctionScannerABC from py_to_uxf_core.abc.function_scanner_abc import FunctionScannerABC
from py_to_uxf_core.abc.implementation_scanner_abc import ImplementationScannerABC
from py_to_uxf_core.abc.python_parser_abc import PythonParserABC from py_to_uxf_core.abc.python_parser_abc import PythonParserABC
from py_to_uxf_core.abc.umlet_creator_abc import UmletCreatorABC from py_to_uxf_core.abc.umlet_creator_abc import UmletCreatorABC
from py_to_uxf_core.service.attribute_scanner_service import AttributeScannerService from py_to_uxf_core.service.attribute_scanner_service import AttributeScannerService
from py_to_uxf_core.service.class_scanner_service import ClassScannerService from py_to_uxf_core.service.class_scanner_service import ClassScannerService
from py_to_uxf_core.service.file_scanner_service import FileScannerService from py_to_uxf_core.service.file_scanner_service import FileScannerService
from py_to_uxf_core.service.function_scanner_service import FunctionScannerService from py_to_uxf_core.service.function_scanner_service import FunctionScannerService
from py_to_uxf_core.service.implementation_scanner_service import ImplementationScannerService
from py_to_uxf_core.service.python_parser_service import PythonParserService from py_to_uxf_core.service.python_parser_service import PythonParserService
from py_to_uxf_core.service.umlet_creator_service import UmletCreatorService from py_to_uxf_core.service.umlet_creator_service import UmletCreatorService
@@ -26,7 +30,17 @@ class Startup(StartupABC):
def configure_configuration(self, configuration: ConfigurationABC, environment: ApplicationEnvironment) -> ConfigurationABC: def configure_configuration(self, configuration: ConfigurationABC, environment: ApplicationEnvironment) -> ConfigurationABC:
environment.set_runtime_directory(os.path.dirname(__file__)) environment.set_runtime_directory(os.path.dirname(__file__))
configuration.add_console_argument(ConsoleArgument('--', 'path', [], ' ', is_value_token_optional=False)) if os.path.isfile('py_to_uxf/appsettings.json'):
configuration.add_json_file('py_to_uxf/appsettings.json', optional=False, output=False)
elif os.path.isfile('appsettings.json'):
configuration.add_json_file('appsettings.json', optional=False, output=False)
else:
Console.error(f'Expected config file appsettings.json')
sys.exit()
configuration.add_console_argument(ConsoleArgument('-', 'p', [], ' ', False, [
ConsoleArgument('-', 'o', [], ' ', is_value_token_optional=False)
]))
configuration.add_console_arguments(error=False) configuration.add_console_arguments(error=False)
return configuration return configuration
@@ -35,6 +49,7 @@ class Startup(StartupABC):
services.add_transient(ClassScannerABC, ClassScannerService) services.add_transient(ClassScannerABC, ClassScannerService)
services.add_transient(FunctionScannerABC, FunctionScannerService) services.add_transient(FunctionScannerABC, FunctionScannerService)
services.add_transient(AttributeScannerABC, AttributeScannerService) services.add_transient(AttributeScannerABC, AttributeScannerService)
services.add_transient(ImplementationScannerABC, ImplementationScannerService)
services.add_singleton(PythonParserABC, PythonParserService) services.add_singleton(PythonParserABC, PythonParserService)
services.add_singleton(UmletCreatorABC, UmletCreatorService) services.add_singleton(UmletCreatorABC, UmletCreatorService)

View File

@@ -0,0 +1,16 @@
from abc import ABC, abstractmethod
from typing import Optional, Union
from cpl_query.extension import List
from py_to_uxf_core.model.class_implementation import ClassImplementation
from py_to_uxf_core.model.python_class import PythonClass
class ImplementationScannerABC(ABC):
@abstractmethod
def __init__(self): pass
@abstractmethod
def scan_line_for_implementation(self, line: str, classes: list[PythonClass]) -> Union[Optional[ClassImplementation], List[ClassImplementation]]: pass

View File

@@ -1,5 +1,10 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from cpl_query.extension import List
from py_to_uxf_core.model.class_implementation import ClassImplementation
from py_to_uxf_core.model.python_class import PythonClass
class PythonParserABC(ABC): class PythonParserABC(ABC):
@@ -7,4 +12,7 @@ class PythonParserABC(ABC):
def __init__(self): pass def __init__(self): pass
@abstractmethod @abstractmethod
def parse(self): pass def parse_classes(self) -> List[PythonClass]: pass
@abstractmethod
def parse_implementations(self, classes: List[PythonClass]) -> List[ClassImplementation]: pass

View File

@@ -1,7 +1,13 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from py_to_uxf_core.model.class_implementation import ClassImplementation
from py_to_uxf_core.model.python_class import PythonClass
class UmletCreatorABC(ABC): class UmletCreatorABC(ABC):
@abstractmethod @abstractmethod
def __init__(self): pass def __init__(self): pass
@abstractmethod
def generate_xml(self, classes: list[PythonClass], implementations: list[ClassImplementation]) -> str: pass

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
"""
py_to_uxf
~~~~~~~~~~~~~~~~~~~
:copyright: (c)
:license:
"""
__title__ = 'py_to_uxf_core.configuration'
__author__ = ''
__license__ = ''
__copyright__ = 'Copyright (c) '
__version__ = '0.0.0'
from collections import namedtuple
# imports
VersionInfo = namedtuple('VersionInfo', 'major minor micro')
version_info = VersionInfo(major='0', minor='0', micro='0')

View File

@@ -0,0 +1,23 @@
import traceback
from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC
from cpl_core.console import Console
class ParserSettings(ConfigurationModelABC):
def __init__(self):
ConfigurationModelABC.__init__(self)
self._ignore_class_names = []
@property
def ignore_class_names(self) -> list[str]:
return self._ignore_class_names
def from_dict(self, settings: dict):
try:
self._ignore_class_names = settings['IgnoreClassNames']
except Exception as e:
Console.error(f'[ ERROR ] [ {__name__} ]: Reading error in {self.__name__} settings')
Console.error(f'[ EXCEPTION ] [ {__name__} ]: {e} -> {traceback.format_exc()}')

View File

@@ -0,0 +1,23 @@
import traceback
from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC
from cpl_core.console import Console
class UMLCreatorSettings(ConfigurationModelABC):
def __init__(self):
ConfigurationModelABC.__init__(self)
self._max_pixel_x = 0
@property
def max_pixel_x(self) -> int:
return self._max_pixel_x
def from_dict(self, settings: dict):
try:
self._max_pixel_x = settings['MaxPixelX']
except Exception as e:
Console.error(f'[ ERROR ] [ {__name__} ]: Reading error in {self.__name__} settings')
Console.error(f'[ EXCEPTION ] [ {__name__} ]: {e} -> {traceback.format_exc()}')

View File

@@ -0,0 +1,23 @@
from py_to_uxf_core.model.python_class import PythonClass
class ClassImplementation:
def __init__(self, base: PythonClass, subclass: PythonClass):
self._base = base
self._subclass = subclass
self.is_first = True
@property
def base(self) -> PythonClass:
return self._base
@property
def subclass(self) -> PythonClass:
return self._subclass
def __str__(self):
return f'<{type(self).__name__} {self.is_first} {self._subclass.name} -> {self._base.name}>'
def __repr__(self):
return f'<{type(self).__name__} {self.is_first} {self._subclass.name} -> {self._base.name}>'

View File

@@ -0,0 +1,27 @@
class Dimension:
def __init__(self, width: int, height: int):
self._width = width
self._height = height
@property
def width(self) -> int:
return self._width
@width.setter
def width(self, value: int):
self._width = value
@property
def height(self) -> int:
return self._height
@height.setter
def height(self, value: int):
self._height = value
def __str__(self):
return f'<{type(self).__name__} w:{self._width}, h:{self._height}>'
def __repr__(self):
return f'<{type(self).__name__} w:{self._width}, h:{self._height}>'

View File

@@ -0,0 +1,27 @@
class Position:
def __init__(self, x: int, y: int):
self._x = x
self._y = y
@property
def x(self) -> int:
return self._x
@x.setter
def x(self, value: int):
self._x = value
@property
def y(self) -> int:
return self._y
@y.setter
def y(self, value: int):
self._y = value
def __str__(self):
return f'<{type(self).__name__} x:{self._x}, y:{self._y}>'
def __repr__(self):
return f'<{type(self).__name__} x:{self._x}, y:{self._y}>'

View File

@@ -19,8 +19,18 @@ class PythonClass:
def functions(self) -> List[PythonFunction]: def functions(self) -> List[PythonFunction]:
return self._functions return self._functions
@property
def attributes(self) -> List[PythonClassAttribute]:
return self._attributes
def add_function(self, func: PythonFunction): def add_function(self, func: PythonFunction):
self._functions.append(func) self._functions.append(func)
def add_attribute(self, attribute: PythonClassAttribute): def add_attribute(self, attribute: PythonClassAttribute):
self._attributes.append(attribute) self._attributes.append(attribute)
def __str__(self):
return f'<{type(self).__name__} {self._name} a:{len(self._attributes)} f:{len(self._functions)}>'
def __repr__(self):
return f'<{type(self).__name__} {self._name} a:{len(self._attributes)} f:{len(self._functions)}>'

View File

@@ -21,7 +21,7 @@ class PythonClassAttribute:
return self._type return self._type
def __str__(self): def __str__(self):
return f'{self._name}: {self._type}' return f'<{type(self).__name__} {self._name}: {self._type}>'
def __repr__(self): def __repr__(self):
return f'{self._name}: {self._type}' return f'<{type(self).__name__} {self._name}: {self._type}>'

View File

@@ -13,7 +13,7 @@ class PythonFunctionArgument:
return self._type return self._type
def __str__(self): def __str__(self):
return f'{self._name}: {self._type}' return f'<{type(self).__name__} {self._name}: {self._type}>'
def __repr__(self): def __repr__(self):
return f'{self._name}: {self._type}' return f'<{type(self).__name__} {self._name}: {self._type}>'

View File

@@ -0,0 +1,130 @@
import math
import textwrap
from cpl_core.console import Console
from py_to_uxf_core.model.dimension import Dimension
from py_to_uxf_core.model.position import Position
from py_to_uxf_core.model.python_class import PythonClass
class UMLClass:
def __init__(
self,
cls: PythonClass,
pos: Position,
dim: Dimension
):
self._cls = cls
self._position = pos
self._dimension = dim
self._attributes = ''
self._functions = ''
@property
def cls(self) -> PythonClass:
return self._cls
@property
def position(self) -> Position:
return self._position
@position.setter
def position(self, value: int):
self._position = value
@property
def dimension(self) -> Dimension:
return self._dimension
@dimension.setter
def dimension(self, value: int):
self._dimension = value
def __str__(self):
return f'<{type(self).__name__} {self._cls}: {self._position} {self._dimension}>'
def __repr__(self):
return f'<{type(self).__name__} {self._cls}: {self._position} {self._dimension}>'
def implementation_as_xml(self, base_class: 'UMLClass') -> str:
base_point = Position(
base_class.position.x + int(base_class.dimension.width / 2),
base_class.position.y + base_class.dimension.height
)
sub_point = Position(
self._position.x + int(self._dimension.width / 2),
self._position.y
)
path_dimension = Dimension(
self._position.x - base_class.position.x,
sub_point.y - base_point.y
)
path_position = Position(
0,
0
)
return f"""\
<element>
<id>Relation</id>
<coordinates>
<x>{base_point.x}</x>
<y>{base_point.y}</y>
<w>0</w>
<h>0</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-
</panel_attributes>
<additional_attributes>{float(path_position.x)};{float(path_position.y)};{float(path_dimension.width)};{float(path_dimension.height)}</additional_attributes>
</element>
""".replace(' ', '').replace('\t', '')
def create_xml_body(self):
px_per_line = 16
px_per_char = 2.9
self._dimension.height += int(
round((self._cls.attributes.count() + self._cls.functions.count()) * px_per_line, -1))
longest_line_length = self._dimension.width / px_per_char
if len(self._cls.attributes) > 0:
for atr in self._cls.attributes:
attribute = f'{atr.access_modifier.value}{atr.name}: {atr.type}\n'
if len(attribute) > longest_line_length:
longest_line_length = len(attribute)
self._attributes += attribute
if len(self._cls.functions) > 0:
for func in self._cls.functions:
args = ''
function = f'{func.access_modifier.value}{func.name}({args}): {func.return_type}\n'
if len(function) > longest_line_length:
longest_line_length = len(function)
self._functions += function
self._dimension.width = int(round(longest_line_length * px_per_char * px_per_char, -1))
def as_xml(self) -> str:
return f"""\
<element>
<id>UMLClass</id>
<coordinates>
<x>{self._position.x}</x>
<y>{self._position.y}</y>
<w>{self._dimension.width}</w>
<h>{self._dimension.height}</h>
</coordinates>
<panel_attributes>
{self._cls.name}
--
{self._attributes}
--
{self._functions}
</panel_attributes>
<additional_attributes></additional_attributes>
</element>
""".replace(' ', '').replace('\t', '')

View File

@@ -26,6 +26,12 @@ class AttributeScannerService(AttributeScannerABC):
if name.startswith('self.'): if name.startswith('self.'):
name = name.split('self.')[1] name = name.split('self.')[1]
if ':' in name:
name = name.split(':')[0]
if '.' in name:
return None
access_modifier = AccessModifierEnum.public access_modifier = AccessModifierEnum.public
type_str = 'any' type_str = 'any'

View File

@@ -12,6 +12,8 @@ class ClassScannerService(ClassScannerABC):
ClassScannerABC.__init__(self) ClassScannerABC.__init__(self)
def scan_line_for_classes(self, line: str) -> Optional[PythonClass]: def scan_line_for_classes(self, line: str) -> Optional[PythonClass]:
line = line.replace(' ', '')
line = line.replace('\t', '')
if line.startswith('class'): if line.startswith('class'):
name = line.split('class ')[1] name = line.split('class ')[1]
if '(' in name: if '(' in name:

View File

@@ -12,7 +12,7 @@ class FileScannerService(FileScannerABC):
def __init__(self, config: ConfigurationABC): def __init__(self, config: ConfigurationABC):
FileScannerABC.__init__(self) FileScannerABC.__init__(self)
self._config = config self._config = config
self._path = config.get_configuration('path') self._path = config.get_configuration('p')
def scan_files(self) -> List[str]: def scan_files(self) -> List[str]:
files = List(str) files = List(str)

View File

@@ -1,6 +1,6 @@
from typing import Optional from typing import Optional
import value as value from cpl_core.console import Console
from cpl_query.extension import List from cpl_query.extension import List
from py_to_uxf_core.abc.function_scanner_abc import FunctionScannerABC from py_to_uxf_core.abc.function_scanner_abc import FunctionScannerABC
@@ -21,46 +21,48 @@ class FunctionScannerService(FunctionScannerABC):
# async def xy(x: int, y: int) -> int: # async def xy(x: int, y: int) -> int:
# async def _xy(x: int, y: int) -> int: # async def _xy(x: int, y: int) -> int:
# async def __xy(x: int, y: int) -> int: # async def __xy(x: int, y: int) -> int:
if line.startswith('def') or line.startswith('async def'): if not line.startswith('def ') and not line.startswith('async def '):
# name return None
name = line.split('def ')[1]
name = name.split('(')[0] line = line.replace('\t', '')
# name
name = line.split('def ')[1]
name = name.split('(')[0]
access_modifier = AccessModifierEnum.public
args = List()
return_type = 'None'
# access_modifier
if name == '__init__' or name == '__repr__' or name == '__str__':
access_modifier = AccessModifierEnum.public access_modifier = AccessModifierEnum.public
args = List() elif name.startswith('_'):
return_type = 'None' access_modifier = AccessModifierEnum.protected
elif name.startswith('__'):
access_modifier = AccessModifierEnum.private
# access_modifier # args
if name == '__init__' or name == '__repr__' or name == '__str__': args_str = line.split('(')[1]
access_modifier = AccessModifierEnum.public args_str = args_str.split(')')[0]
elif name.startswith('_'): # multiple arguments
access_modifier = AccessModifierEnum.protected if ',' in args_str:
elif name.startswith('__'): for argument in args_str.split(','):
access_modifier = AccessModifierEnum.private # with type
type_str = 'any'
arg_name = argument
if ':' in argument:
type_str = argument.split(': ')[1]
arg_name = argument.split(': ')[0]
# without type
if arg_name.startswith(' '):
arg_name = arg_name.split(' ')[1]
args.append(PythonFunctionArgument(arg_name, type_str))
# single argument
elif args_str != '':
pass
# args # return_type
args_str = line.split('(')[1] if '->' in line:
args_str = args_str.split(')')[0] return_type_str = line.split('-> ')[1]
# multiple arguments return_type = return_type_str.split(':')[0]
if ',' in args_str:
for argument in args_str.split(','):
# with type
type_str = 'any'
arg_name = argument
if ':' in argument:
type_str = argument.split(': ')[1]
arg_name = argument.split(': ')[0]
# without type
if arg_name.startswith(' '):
arg_name = arg_name.split(' ')[1]
args.append(PythonFunctionArgument(arg_name, type_str))
# single argument
elif args_str != '':
pass
# return_type return PythonFunction(access_modifier, name, args, return_type)
if '->' in line:
return_type_str = line.split('-> ')[1]
return_type = return_type_str.split(':')[0]
return PythonFunction(access_modifier, name, args, return_type)
return None

View File

@@ -0,0 +1,56 @@
from typing import Optional, Union
from cpl_core.configuration import ConfigurationABC
from cpl_core.console import Console
from cpl_query.extension import List
from py_to_uxf_core.abc.implementation_scanner_abc import ImplementationScannerABC
from py_to_uxf_core.configuration.parser_settings import ParserSettings
from py_to_uxf_core.model.class_implementation import ClassImplementation
from py_to_uxf_core.model.python_class import PythonClass
class ImplementationScannerService(ImplementationScannerABC):
def __init__(self, configuration: ConfigurationABC):
ImplementationScannerABC.__init__(self)
self._parser_settings: ParserSettings = configuration.get_configuration(ParserSettings)
def _get_implementation(self, base_name: str, sub_class: PythonClass, classes: List[PythonClass]) -> Optional[ClassImplementation]:
base: Optional[PythonClass] = classes.where(lambda c: c.name == base_name).first_or_default()
if base_name in self._parser_settings.ignore_class_names:
return None
if base is None:
new_base = PythonClass(base_name)
classes.append(new_base)
return ClassImplementation(new_base, sub_class)
return ClassImplementation(base, sub_class)
def scan_line_for_implementation(self, line: str, classes: List[PythonClass]) -> Union[Optional[ClassImplementation], List[ClassImplementation]]:
line = line.replace(' ', '')
line = line.replace('\t', '')
if line.startswith('class ') and '(' in line and ')' in line:
sub_name = line.split('class ')[1].split('(')[0]
subclasses = classes.where(lambda c: c.name == sub_name)
implementations = List(ClassImplementation)
for sub in subclasses:
line = line.replace(' ', '')
base_name = line.split('(')[1].split(')')[0]
if ',' not in base_name:
implementation = self._get_implementation(base_name, sub, classes)
if implementation is not None:
implementations.append(implementation)
continue
for name in base_name.split(','):
if '=' in name:
continue
implementation = self._get_implementation(name, sub, classes)
if implementation is not None:
implementation.is_first = False
implementations.append(implementation)
return implementations
return None

View File

@@ -1,12 +1,17 @@
import sys
import traceback
from typing import Optional from typing import Optional
from cpl_core.console import Console from cpl_core.console import Console
from cpl_query.extension import List
from py_to_uxf_core.abc.attribute_scanner_abc import AttributeScannerABC from py_to_uxf_core.abc.attribute_scanner_abc import AttributeScannerABC
from py_to_uxf_core.abc.class_scanner_abc import ClassScannerABC from py_to_uxf_core.abc.class_scanner_abc import ClassScannerABC
from py_to_uxf_core.abc.file_scanner_abc import FileScannerABC from py_to_uxf_core.abc.file_scanner_abc import FileScannerABC
from py_to_uxf_core.abc.function_scanner_abc import FunctionScannerABC from py_to_uxf_core.abc.function_scanner_abc import FunctionScannerABC
from py_to_uxf_core.abc.implementation_scanner_abc import ImplementationScannerABC
from py_to_uxf_core.abc.python_parser_abc import PythonParserABC from py_to_uxf_core.abc.python_parser_abc import PythonParserABC
from py_to_uxf_core.model.class_implementation import ClassImplementation
from py_to_uxf_core.model.python_class import PythonClass from py_to_uxf_core.model.python_class import PythonClass
@@ -17,7 +22,8 @@ class PythonParserService(PythonParserABC):
file_scanner: FileScannerABC, file_scanner: FileScannerABC,
class_scanner: ClassScannerABC, class_scanner: ClassScannerABC,
function_scanner: FunctionScannerABC, function_scanner: FunctionScannerABC,
attribute_scanner: AttributeScannerABC attribute_scanner: AttributeScannerABC,
implementation_scanner: ImplementationScannerABC
): ):
PythonParserABC.__init__(self) PythonParserABC.__init__(self)
@@ -25,54 +31,125 @@ class PythonParserService(PythonParserABC):
self._class_scanner = class_scanner self._class_scanner = class_scanner
self._function_scanner = function_scanner self._function_scanner = function_scanner
self._attribute_scanner = attribute_scanner self._attribute_scanner = attribute_scanner
self._implementation_scanner = implementation_scanner
def parse(self): self._files_with_classes: list[str] = []
def parse_classes(self) -> List[PythonClass]:
files = self._file_scanner.scan_files() files = self._file_scanner.scan_files()
classes = List(PythonClass)
for file in files: for file in files:
Console.write_line('\nfi:', file)
is_comment = False is_comment = False
is_function = False
with open(file, 'r') as file_content: with open(file, 'r') as file_content:
cls: Optional[PythonClass] = None cls: Optional[PythonClass] = None
for line in file_content.readlines(): lines = file_content.readlines()
line = line.replace(' ', '')
line = line.replace('\t', '')
# replace line break at the end of line
if line.endswith('\n'):
line = line.replace('\n', '')
if line == '\n' or line == '': for i in range(len(lines)):
continue try:
line = lines[i]
line_with_tabs = line.replace(' ', '\t')
line = line.replace(' ', '')
line = line.replace('\t', '')
# replace line break at the end of line
if line.endswith('\n'):
line = line.replace('\n', '')
# one line comments if line == '\n' or line == '':
if line != '"""' and line.startswith('"""') and line.endswith('"""') or line.startswith('#'): continue
continue
# start multi line comment # one line comments
if line.startswith('"""') and not is_comment: if line != '"""' and line.startswith('"""') and line.endswith('"""') or line.startswith('#'):
is_comment = True continue
continue
# end multi line comment # start multi line comment
if line.startswith('"""') or line.endswith('"""') and is_comment: if line.startswith('"""') or line.count('"""') == 1 and not is_comment:
is_comment = False is_comment = True
continue continue
if is_comment: # end multi line comment
continue if line.startswith('"""') or line.endswith('"""') or line.count('"""') == 1 and is_comment:
is_comment = False
continue
if cls is None: if is_comment:
cls = self._class_scanner.scan_line_for_classes(line) continue
if cls is not None:
Console.write_line('cl:', cls.name)
continue
func = self._function_scanner.scan_line_for_function(line) if cls is None:
if func is not None: cls = self._class_scanner.scan_line_for_classes(line)
cls.add_function(func) if cls is not None:
Console.write_line('fu:', func.access_modifier.value, func.name, func.args, func.return_type) classes.append(cls)
continue self._files_with_classes.append(file)
continue
attribute = self._attribute_scanner.scan_line_for_attribute(line) func = self._function_scanner.scan_line_for_function(line)
if attribute is not None: if func is not None:
cls.add_attribute(attribute) cls.add_function(func)
Console.write_line('at:', attribute.name, attribute.type) is_function = True if func.name != '__init__' else False
continue
if is_function:
continue
attribute = self._attribute_scanner.scan_line_for_attribute(line)
if attribute is not None:
cls.add_attribute(attribute)
except Exception as e:
Console.error(f'Parsing {file}@{i}', f'{e} -> {traceback.format_exc()}')
file_content.close()
sys.exit()
file_content.close()
return classes
def parse_implementations(self, classes: List[PythonClass]) -> List[ClassImplementation]:
implementations = List()
files = self._files_with_classes
for file in files:
is_comment = False
with open(file, 'r') as file_content:
implementation: Optional[ClassImplementation] = None
lines = file_content.readlines()
for i in range(len(lines)):
try:
line = lines[i]
line = line.replace(' ', '')
line = line.replace('\t', '')
# replace line break at the end of line
if line.endswith('\n'):
line = line.replace('\n', '')
if line == '\n' or line == '':
continue
# one line comments
if line != '"""' and line.startswith('"""') and line.endswith('"""') or line.startswith('#'):
continue
# start multi line comment
if line.startswith('"""') or line.count('"""') == 1 and not is_comment:
is_comment = True
continue
# end multi line comment
if line.startswith('"""') or line.endswith('"""') or line.count('"""') == 1 and is_comment:
is_comment = False
continue
if is_comment:
continue
if implementation is None:
implementation = self._implementation_scanner.scan_line_for_implementation(line, classes)
if implementation is not None:
if isinstance(implementation, List):
for impl in implementation:
implementations.append(impl)
else:
implementations.append(implementation)
except Exception as e:
Console.error(f'Parsing {file}@{i}', f'{e} -> {traceback.format_exc()}')
file_content.close()
sys.exit()
file_content.close()
return implementations

View File

@@ -1,7 +1,116 @@
from cpl_core.configuration import ConfigurationABC
from cpl_core.console import Console
from cpl_query.extension import List
from py_to_uxf_core.abc.umlet_creator_abc import UmletCreatorABC from py_to_uxf_core.abc.umlet_creator_abc import UmletCreatorABC
from py_to_uxf_core.configuration.uml_creator_settings import UMLCreatorSettings
from py_to_uxf_core.model.class_implementation import ClassImplementation
from py_to_uxf_core.model.dimension import Dimension
from py_to_uxf_core.model.position import Position
from py_to_uxf_core.model.python_class import PythonClass
from py_to_uxf_core.model.uml_class import UMLClass
class UmletCreatorService(UmletCreatorABC): class UmletCreatorService(UmletCreatorABC):
def __init__(self): def __init__(self, config: ConfigurationABC):
UmletCreatorABC.__init__(self) UmletCreatorABC.__init__(self)
self._settings: UMLCreatorSettings = config.get_configuration(UMLCreatorSettings)
self._moved_classes: list[UMLClass] = []
self._padding = 30
def _sort_by_implementation(self, uml_classes: List[UMLClass], implementations: List[ClassImplementation]):
base_sub_map: dict[UMLClass, list[UMLClass]] = {}
for implementation in implementations:
sub_class: UMLClass = uml_classes.where(lambda u: u.cls == implementation.subclass).first()
base_class: UMLClass = uml_classes.where(lambda u: u.cls == implementation.base).first()
if base_class not in base_sub_map:
base_sub_map[base_class] = []
if sub_class in base_sub_map[base_class]:
continue
base_sub_map[base_class].append(sub_class)
last_base = Position(self._padding, self._padding)
highest_y = self._padding
for base in base_sub_map:
subclasses = base_sub_map[base]
base.position.x = last_base.x
base.position.y = last_base.y
last_sub = Position(base.position.x, base.position.y + base.dimension.height)
for sub_class in subclasses:
if sub_class not in self._moved_classes:
sub_class.position.x = base.position.x
sub_class.position.y = base.position.y
if sub_class.position.x <= last_sub.x:
sub_class.position.x = last_sub.x
if sub_class.position.y <= last_sub.y:
sub_class.position.y = last_sub.y + self._padding * 2
last_sub.x = sub_class.position.x + sub_class.dimension.width + self._padding
if sub_class.position.y + sub_class.dimension.height > highest_y:
highest_y = sub_class.position.y + sub_class.dimension.height
self._moved_classes.append(sub_class)
last_base.x = last_sub.x
if last_base.x <= self._settings.max_pixel_x:
last_base.x += self._padding * 2
else:
last_base.x = self._padding
last_base.y = highest_y + self._padding * 2
self._moved_classes.append(base)
for cls in uml_classes:
if cls in self._moved_classes:
continue
new_x = last_base.x
if new_x <= self._settings.max_pixel_x:
cls.position.x = new_x
last_base.x = new_x + cls.dimension.width + self._padding
cls.position.y = last_base.y
else:
cls.position.x = self._padding
cls.position.y = highest_y + self._padding * 2
last_base = cls.position
if cls.position.y + cls.dimension.height > highest_y:
highest_y = cls.position.y + cls.dimension.height
def generate_xml(self, classes: List[PythonClass], implementations: List[ClassImplementation]) -> str:
uml_classes = List(UMLClass)
xml_classes = ''
xml_relations = ''
for cls in classes:
uml_cls = UMLClass(cls, Position(0, self._padding), Dimension(80, 80))
# trigger xml generation to render coordinates
uml_cls.create_xml_body()
uml_classes.append(uml_cls)
self._sort_by_implementation(uml_classes, implementations)
for implementation in implementations:
sub_class: UMLClass = uml_classes.where(lambda u: u.cls == implementation.subclass).first()
base_class: UMLClass = uml_classes.where(lambda u: u.cls == implementation.base).first()
xml_relations += sub_class.implementation_as_xml(base_class)
for uml_cls in uml_classes:
# save class xml
xml_classes += uml_cls.as_xml()
return f"""\
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<diagram program="umlet" version="14.1.1">
<zoom_level>10</zoom_level>
{xml_classes}
{xml_relations}
</diagram>
""".replace(' ', '').replace('\t', '')