import astroid
import docstring_parser as dp
import importlib
import os
import pip
import requests
import shutil
import sys
import tarfile
S = " "
MAX_LEN = 125
def pretty_printer(string: str, base_off=" ", additional_off=" "):
bo = base_off
ao = base_off + additional_off
new_lines = list()
for line in string.split("\n"):
i = MAX_LEN
line = bo + line
while i < len(line):
line = line[:i] + "\n" + ao + line[i:]
i += MAX_LEN
new_lines.append(line)
print("\n".join(new_lines))
ASTROID_CACHE_BACKUP = astroid.astroid_manager.MANAGER._mod_file_cache.copy()
Per come si prevede di lavorare, con una lista di moduli da parsare, potrebbe capitare di rifinire nello stesso modulo, e nello stesso nodo del suo AST più volte per via dei riferimenti. Un tipo definito in un modulo può essere richiamato più voltem e più volte si finirà in tale nodo per creare l'individuo di tale tipo. Tale operazione va fatta solo una volta. Se l'AST è sempre lo stesso si potrebbe pensare di appendere un nuovo attributo al nodo, per memorizzare l'individuo o gli individui ad esso associati, e così recuperare l'individuo senza ricrearlo.
Facciamo modo di ricondurci in tre modi diversi allo stesso modulo, creando tre AST.
path_1 = "same_ast_md_1.py"
name_1 = os.path.splitext(".".join(os.path.split(path_1)))[0][1:]
path_2 = "same_ast_md_2.py"
name_2 = os.path.splitext(".".join(os.path.split(path_2)))[0]
path_3 = "same_ast_md_3.py"
name_3 = os.path.splitext(".".join(os.path.split(path_3)))[0]
with open(path_1, "r", encoding="utf8") as f1, \
open(path_2, "r", encoding="utf8") as f2, \
open(path_3, "r", encoding="utf8") as f3:
code_text_1 = f1.read()
code_text_2 = f2.read()
code_text_3 = f3.read()
t = '\n'.join([S+line for line in code_text_1.split('\n')]); print(f"Modulo 1:\n{t}")
t = '\n'.join([S+line for line in code_text_2.split('\n')]); print(f"Modulo 2:\n{t}")
t = '\n'.join([S+line for line in code_text_3.split('\n')]); print(f"Modulo 3:\n{t}")
tree_1 = astroid.parse(code_text_1, path=path_1, module_name=name_1)
tree_2 = astroid.parse(code_text_2, path=path_2, module_name=name_2)
tree_3 = astroid.parse(code_text_3, path=path_3, module_name=name_3)
Modulo 1: import same_ast_md_2 pass Modulo 2: import same_ast_md_3 pass Modulo 3: pass
astroid.astroid_manager.MANAGER.astroid_cache
{'builtins': <Module.builtins l.0 at 0x17222d548b0>, '': <Module l.0 at 0x172248f9e20>, 'same_ast_md_1': <Module.same_ast_md_1 l.0 at 0x17224b96d60>, '.same_ast_md_2': <Module..same_ast_md_2 l.0 at 0x17224b96e50>, '.same_ast_md_3': <Module..same_ast_md_3 l.0 at 0x17224b96bb0>}
Creaiamo un altro AST del modulo same_ast_md_3 dall'import import del codice nel modulo same_ast_md_1.
tree_md_3_1 = None
for child in tree_1.get_children():
if isinstance(child, astroid.nodes.Import):
mod_name = child.names[0][0]
tree_md_3_1 = child.do_import_module(mod_name)
Si può vedere che sono esattamente gli stessi alberi.
tree_3 is tree_md_3_1
False
Mentre non sono gli stessi alberi quelli di same_ast_md_1.py e same_ast_md_2.py.
tree_1 is tree_2
False
Proviamo ad aggiungere un attributo all'albero di same_ast_md_3, ricreiamo un'altro suo albero dall'import nel modulo same_ast_md_2 (non ancora usato) e vediamo se anché lì è presente l'attributo aggiunto.
for child in tree_3.get_children():
if isinstance(child, astroid.nodes.Pass):
child.fake_attribute = "Aggiunto un attributo non originale dell'AST"
print(child.fake_attribute)
tree_md_3_2 = None
for child in tree_2.get_children():
if isinstance(child, astroid.nodes.Import):
mod_name = child.names[0][0]
tree_md_3_2 = child.do_import_module(mod_name)
for child in tree_md_3_1.get_children():
if isinstance(child, astroid.nodes.Pass):
print(getattr(child, "fake_attribute", None))
for child in tree_md_3_2.get_children():
if isinstance(child, astroid.nodes.Pass):
print(getattr(child, "fake_attribute", None))
Aggiunto un attributo non originale dell'AST None None
Possiamo quindi aggiungere gli individui direttamente ai nodi dell'AST per ritrovarli poi più facilmente.
L'idea è che da ogni tipo di nodo dell'AST possono generarsi diversi concetti dell'ontologia a cui si può assegnare un IRI univoco a partire dal nodo che lo genera. Si pensa ad una funzione deterministica get_iri(node, ontology_class) che prende il nodo di partenzza e il tipo di individuo da creare per generare il suo IRI a partire dalle informazioni del nodo.
# TODO
Dare i credits a https://stackoverflow.com/a/24236320/13640701
from contextlib import contextmanager
from unittest import mock
import setuptools
from setuptools.config import read_configuration
import importlib
@contextmanager
def safe_setup_read(abs_setup_path):
# Backup of current working directory, sys.stdout and sys.stderr
saved_cwd = os.getcwd()
saved_stdout, saved_stderr = sys.stdout, sys.stderr
# saved_sys_modules = sys.modules.copy()
# Move to setup directory and deactivate output prints
os.chdir(abs_setup_path)
sys.stdout = sys.stderr = open(os.devnull, "w")
try:
yield
finally:
# Restore saved values
os.chdir(saved_cwd)
sys.stdout, sys.stderr = saved_stdout, saved_stderr
# Clear imported modules caches
del sys.modules["setup"]# sys.modules = saved_sys_modules
targets = ["sphinx", "matplotlib"]
target = targets[0]
importlib.invalidate_caches()
with safe_setup_read(os.path.abspath(target)), \
mock.patch.object(setuptools, 'setup') as mock_setup:
try:
print(os.getcwd())
setup = importlib.import_module("setup")
print(setup)
_, kwargs = mock_setup.call_args
except:
pass
finally:
del setup
print(os.getcwd())
D:\Coding\Jupyter\Py39\ASTROID
dir().count("setup")
0
sys.modules.get("setup", "None")
'None'
Le chiavi permesse sono disponibili a https://setuptools.pypa.io/en/latest/references/keywords.html
for k in kwargs:
print(f"\n{k}:")
pretty_printer(f"{kwargs.get(k, None)}", base_off=" ", additional_off="")
name: Sphinx version: 5.2.0 url: https://www.sphinx-doc.org/ download_url: https://pypi.org/project/Sphinx/ license: BSD author: Georg Brandl author_email: georg@python.org description: Python documentation generator long_description: ======== Sphinx ======== .. image:: https://img.shields.io/pypi/v/sphinx.svg :target: https://pypi.org/project/Sphinx/ :alt: Package on PyPI .. image:: https://github.com/sphinx-doc/sphinx/actions/workflows/main.yml/badge.svg :target: https://github.com/sphinx-doc/sphinx/actions/workflows/main.yml :alt: Build Status .. image:: https://readthedocs.org/projects/sphinx/badge/?version=master :target: https://www.sphinx-doc.org/ :alt: Documentation Status .. image:: https://img.shields.io/badge/License-BSD%202--Clause-blue.svg :target: https://opensource.org/licenses/BSD-2-Clause :alt: BSD 2 Clause **Sphinx makes it easy to create intelligent and beautiful documentation.** Sphinx uses reStructuredText as its markup language, and many of its strengths come from the power and straightforwardness of reStructuredText and its parsing and translating suite, the Docutils. Features ======== * **Output formats**: HTML, PDF, plain text, EPUB, TeX, manual pages, and more * **Extensive cross-references**: semantic markup and automatic links for functions, classes, glossary terms and similar pieces of information * **Hierarchical structure**: easy definition of a document tree, with automatic links to siblings, parents and children * **Automatic indices**: general index as well as a module index * **Code highlighting**: automatic highlighting using the Pygments highlighter * **Templating**: Flexible HTML output using the Jinja 2 templating engine * **Extension ecosystem**: Many extensions are available, for example for automatic function documentation or working with Jupyter notebooks. * **Language Support**: Python, C, C++, JavaScript, mathematics, and many other languages through extensions. For more information, refer to the `the documentation`_. Installation ============ The following command installs Sphinx from the `Python Package Index`_. You will need a working installation of Python and pip. .. code-block:: sh pip install -U sphinx Contributing ============ We appreciate all contributions! Refer to `the contributors guide`_ for information. Release signatures ================== Releases are signed with following keys: * `498D6B9E <https://pgp.mit.edu/pks/lookup?op=vindex&search=0x102C2C17498D6B9E>`_ * `5EBA0E07 <https://pgp.mit.edu/pks/lookup?op=vindex&search=0x1425F8CE5EBA0E07>`_ * `61F0FB52 <https://pgp.mit.edu/pks/lookup?op=vindex&search=0x52C8F72A61F0FB52>`_ .. _the documentation: https://www.sphinx-doc.org/ .. _the contributors guide: https://www.sphinx-doc.org/en/master/internals/contributing.html .. _Python Package Index: https://pypi.org/project/Sphinx/ long_description_content_type: text/x-rst project_urls: {'Code': 'https://github.com/sphinx-doc/sphinx', 'Changelog': 'https://www.sphinx-doc.org/en/master/changes.html', 'Issue tracker': 'https://github.com/sphinx-doc/sphinx/issues'} zip_safe: False classifiers: ['Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Environment :: Web Environment', 'Intended Aud ience :: Developers', 'Intended Audience :: Education', 'Intended Audience :: End Users/Desktop', 'Intended Audience :: Science/Research', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: BSD License', 'Operating Sy stem :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programm ing Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Frame work :: Setuptools Plugin', 'Framework :: Sphinx', 'Framework :: Sphinx :: Extension', 'Framework :: Sphinx :: Theme', ' Topic :: Documentation', 'Topic :: Documentation :: Sphinx', 'Topic :: Internet :: WWW/HTTP :: Site Management', 'Topic :: Printing', 'Topic :: Software Development', 'Topic :: Software Development :: Documentation', 'Topic :: Text Processi ng', 'Topic :: Text Processing :: General', 'Topic :: Text Processing :: Indexing', 'Topic :: Text Processing :: Markup' , 'Topic :: Text Processing :: Markup :: HTML', 'Topic :: Text Processing :: Markup :: LaTeX', 'Topic :: Utilities'] platforms: any packages: ['sphinx', 'sphinx.builders', 'sphinx.cmd', 'sphinx.directives', 'sphinx.domains', 'sphinx.environment', 'sphinx.ext', 's phinx.locale', 'sphinx.pycode', 'sphinx.search', 'sphinx.testing', 'sphinx.transforms', 'sphinx.util', 'sphinx.writers', 'sphinx.builders.html', 'sphinx.builders.latex', 'sphinx.environment.adapters', 'sphinx.environment.collectors', 'sphin x.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.napoleon', 'sphinx.transforms.post_transforms', 'sphinx.util.stemm er'] package_data: {'sphinx': ['py.typed']} include_package_data: True entry_points: {'console_scripts': ['sphinx-build = sphinx.cmd.build:main', 'sphinx-quickstart = sphinx.cmd.quickstart:main', 'sphinx-ap idoc = sphinx.ext.apidoc:main', 'sphinx-autogen = sphinx.ext.autosummary.generate:main'], 'distutils.commands': ['build_ sphinx = sphinx.setup_command:BuildDoc']} python_requires: >=3.6 install_requires: ['sphinxcontrib-applehelp', 'sphinxcontrib-devhelp', 'sphinxcontrib-jsmath', 'sphinxcontrib-htmlhelp>=2.0.0', 'sphinxcont rib-serializinghtml>=1.1.5', 'sphinxcontrib-qthelp', 'Jinja2>=2.3', 'Pygments>=2.0', 'docutils>=0.14,<0.20', 'snowballst emmer>=1.1', 'babel>=1.3', 'alabaster>=0.7,<0.8', 'imagesize', 'requests>=2.5.0', 'packaging', "importlib-metadata>=4.4; python_version < '3.10'"] extras_require: {':sys_platform=="win32"': ['colorama>=0.3.5'], 'docs': ['sphinxcontrib-websupport'], 'lint': ['flake8>=3.5.0', 'flake8-c omprehensions', 'flake8-bugbear', 'isort', 'mypy>=0.971', 'sphinx-lint', 'docutils-stubs', 'types-typed-ast', 'types-req uests'], 'test': ['pytest>=4.6', 'html5lib', "typed_ast; python_version < '3.8'", 'cython']}
Per identificare le classi da cui una classe eredita, in quanto individui, è necessario individuare il loro nodo definizione ClassDef in un AST.
path_1 = "clsh_md_1.py"
name_1 = os.path.splitext(os.path.split(path_1)[1])[0]
path_2 = "clsh_md_2.py"
with open(path_1, "r", encoding="utf8") as f1, \
open(path_2, "r", encoding="utf8") as f2:
code_text_1 = f1.read()
code_text_2 = f2.read()
t = '\n'.join([S+line for line in code_text_1.split('\n')]); print(f"Modulo 1:\n{t}")
t = '\n'.join([S+line for line in code_text_2.split('\n')]); print(f"Modulo 2:\n{t}")
tree = astroid.parse(code_text_1, path=path_1, module_name=name_1)
Modulo 1: from clsh_md_2 import C class A: pass class B(A): pass class D(B, C): pass Modulo 2: class C: pass
for child in tree.get_children():
if isinstance(child, astroid.nodes.ClassDef):
print("Gerarchia completa:")
for base in child.ancestors():
print(f"{S}{base.name} ({type(base)})")
if base.name == "C":
print(f"{S*2}{base.parent}")
print("Gerarchia diretta:")
for base in child.ancestors(recurs=False):
print(f"{S}{base.name} ({type(base)})")
if base.name == "C":
print(f"{S*2}{base.parent}")
print()
Gerarchia completa: object (<class 'astroid.nodes.scoped_nodes.scoped_nodes.ClassDef'>) Gerarchia diretta: object (<class 'astroid.nodes.scoped_nodes.scoped_nodes.ClassDef'>) Gerarchia completa: A (<class 'astroid.nodes.scoped_nodes.scoped_nodes.ClassDef'>) object (<class 'astroid.nodes.scoped_nodes.scoped_nodes.ClassDef'>) Gerarchia diretta: A (<class 'astroid.nodes.scoped_nodes.scoped_nodes.ClassDef'>) Gerarchia completa: B (<class 'astroid.nodes.scoped_nodes.scoped_nodes.ClassDef'>) A (<class 'astroid.nodes.scoped_nodes.scoped_nodes.ClassDef'>) object (<class 'astroid.nodes.scoped_nodes.scoped_nodes.ClassDef'>) C (<class 'astroid.nodes.scoped_nodes.scoped_nodes.ClassDef'>) Module.clsh_md_2(name='clsh_md_2', doc=None, file='D:\\Coding\\Jupyter\\Py39\\ASTROID\\clsh_md_2.py', path=['D:\\Coding\\Jupyter\\Py39\\ASTROID\\clsh_md_2.py'], package=False, pure_python=True, future_imports=set(), doc_node=None, body=[<ClassDef.C l.2 at 0x172212c5e50>]) Gerarchia diretta: B (<class 'astroid.nodes.scoped_nodes.scoped_nodes.ClassDef'>) C (<class 'astroid.nodes.scoped_nodes.scoped_nodes.ClassDef'>) Module.clsh_md_2(name='clsh_md_2', doc=None, file='D:\\Coding\\Jupyter\\Py39\\ASTROID\\clsh_md_2.py', path=['D:\\Coding\\Jupyter\\Py39\\ASTROID\\clsh_md_2.py'], package=False, pure_python=True, future_imports=set(), doc_node=None, body=[<ClassDef.C l.2 at 0x172212c5e50>])
Quindi utilizzando l'attributo bases si ottengono i nomi degli eredi diretti, mentre dal metodo ancestors si ottengono tutti i nodi ClassDef dagli AST dei moduli in cui tali classi sono definite. Passando il parametro recursive=False al metodo ancestors si ottengono i soli eredi diretti.
In caso di ereditarietà (o anche peggio, ereditarietà multipla), significa individuare il nodo FunctionDef della superclasse che già definisce tale metodo.
path_1 = "override_md.py"
name_1 = os.path.splitext(os.path.split(path_1)[1])[0]
with open(path_1, "r", encoding="utf8") as f1:
code_text_1 = f1.read()
t = '\n'.join([S+line for line in code_text_1.split('\n')]); print(f"Modulo 1:\n{t}")
tree = astroid.parse(code_text_1, path=path_1, module_name=name_1)
Modulo 1: class A: f: int = 0 def g(self): print("A") class B: def f(self): print("B") class C: def f(self): print("C") class D(A, B, C): def f(self): print("D")
Nota bene: siccome f() è presente sia in B che C, e D estende entrambe le classi, quale f eredita e sovrascrive? Siccome eredita nell'ordine (B, C) eredita la sola implementazione di f() presente in B. Se l'ordine fosse inverso (class C(A, C, B)) erediterebbe la sola implementazione di C. Questo è importante per sapere di quale metodo si fa l'override. L'attributo f in A è invece perduto, poiché mascherato dalla funzione con lo stesso nome. Se non ci fosse invece override, essendo l'f di A il primo f che si incontra, sarebbe l'attributo ad essere ereditato da D e non la funzione, che invece andrebbe persa poiché mascherata dall'attributo.
def get_overridden_method(node: astroid.nodes.FunctionDef):
"""
Se restituisce None non ve ne sono, altrimenti restituisce un FunctionDef
"""
assert isinstance(node, astroid.nodes.FunctionDef)
overridden_method_node = None
class_owner = None
if isinstance(node.parent, astroid.nodes.ClassDef):
# print(f"[DEBUG] {node.parent.mro()}")
ancestors_mro = node.parent.mro()[1:] # Salta se stesso
for ancestor_node in ancestors_mro: # Salta il primo erede che è se stesso
for ancestor_method_node in ancestor_node.methods():
if ancestor_method_node.name == node.name:
overridden_method_node = ancestor_method_node
class_owner = ancestor_node
break
if overridden_method_node:
break
if overridden_method_node:
print(f"[DEBUG] '{node.parent.name}' ha eredi ({[a.name for a in ancestors_mro]}) e il suo metodo '{node.name}' sovrascrive quello della classe '{class_owner.name}'")
else:
print(f"[DEBUG] '{node.parent.name}' ha eredi ({[a.name for a in ancestors_mro]}), ma nessuno definisce una tale funzione '{node.name}'")
else:
print(f"[DEBUG] '{node.name}' non è un metodo di una classe, non può fare l'override di nulla!")
return overridden_method_node
def visit(node):
if isinstance(node, astroid.nodes.FunctionDef):
print()
r = get_overridden_method(node)
print(r)
for child in node.get_children():
visit(child)
visit(tree)
[DEBUG] 'A' ha eredi (['object']), ma nessuno definisce una tale funzione 'g' None [DEBUG] 'B' ha eredi (['object']), ma nessuno definisce una tale funzione 'f' None [DEBUG] 'C' ha eredi (['object']), ma nessuno definisce una tale funzione 'f' None [DEBUG] 'D' ha eredi (['A', 'B', 'C', 'object']) e il suo metodo 'f' sovrascrive quello della classe 'B' FunctionDef.f(name='f', doc=None, position=Position(lineno=10, col_offset=1, end_lineno=10, end_col_offset=6), decorators=None, args=<Arguments l.10 at 0x172212c6a60>, returns=None, doc_node=None, body=[<Expr l.11 at 0x172212c6d90>])
Definire i parametri di un metodo significa individuarne il nome e l'eventuale tipo, inducendolo dalle annotazione o per inferenza sul suo uso nel metodo. Altre informazioni possono essere estratte dai commenti, ma trattiamo i commenti a parte. Identificarne il nome è banale, meno banale è risalire al tipo quando segnato in una annotazione.
path_1 = "params_md_1.py"
name_1 = os.path.splitext(os.path.split(path_1)[1])[0]
path_2 = "params_pkg/params_md_2.py"
path_3 = "params_pkg/params_nested_pkg/params_md_3.py"
with open(path_1, "r", encoding="utf8") as f1, \
open(path_2, "r", encoding="utf8") as f2, \
open(path_3, "r", encoding="utf8") as f3:
code_text_1 = f1.read()
code_text_2 = f2.read()
code_text_3 = f3.read()
t = '\n'.join([S+line for line in code_text_1.split('\n')]); print(f"Modulo 1:\n{t}")
t = '\n'.join([S+line for line in code_text_2.split('\n')]); print(f"Modulo 2:\n{t}")
t = '\n'.join([S+line for line in code_text_3.split('\n')]); print(f"Modulo 3:\n{t}")
tree = astroid.parse(code_text_1, path=path_1, module_name=name_1)
Modulo 1: from typing import * from params_pkg import params_md_2 as fake_alias def f(w, x: fake_alias.MyType.NestedMyType, y: Tuple[List[str], Tuple[int, int] | float | Exception] | int | List[str | int], z: fake_alias.AliasMyType) -> str: v = 1 + w return f"{w}" def g(pos_only_1, /, normal_1=1, *var_args, kw_only_1=2, **kw_args): pass c = fake_alias.MyType.NestedMyType() d = fake_alias.AliasMyType() Modulo 2: from params_pkg.params_nested_pkg.params_md_3 import * AliasMyType = MyType Modulo 3: class MyType: def __init__(self): print(f"Hello from {self.__class__.__name__}") class NestedMyType: def __init__(self): print(f"Hello from {self.__class__.__name__}")
def build_type_annotation(ann, prev=(None, tuple(),)):
"""
Restituisce una stringa, una tupla o una lista:
- a) una stringa indica un tipo non parametrico, o di cui non sono stati specificati tipi che lo parametrizzano;
- b) se si ottiene una lista vuol dire che più tipi equivalenti sono ammessi, ed è coinvolto l'operatore di unione
(union types) nell'annotazione;
- c) una tupla indica la presenza di tipi parametrici, dove il primo elemento della tupla è una stringa che indica
il tipo parametrico, e ogni elemento a seguire è uno dei tipi che lo parametrizzano, rappresentato
ricorsivamente come descritto (quindi può essere una stringa, una lista o una tupla).
Tuple[List[str], Tuple[int, int] | float | Exception] | List
[(Tuple, (List, str), [(Tuple, int, int), float, exception]), List]
"""
if ann is None:
return None
# --- a) stringa
if isinstance(ann, astroid.nodes.Name):
# Direttamente il tipo, come 'str'
# print(f"[DEBUG] {ann}")
return ann.name
elif isinstance(ann, astroid.nodes.Attribute):
# Il tipo è dentro un modulo o una classe, come 'typing.List'
# print(f"[DEBUG] {ann}")
name = [ann.attrname]
ann = ann.expr
while isinstance(ann, astroid.nodes.Attribute):
name.insert(0, ann.attrname)
ann = ann.expr
assert isinstance(ann, astroid.nodes.Name)
name.insert(0, ann.name)
return ".".join(name)
# --- b) lista
elif isinstance(ann, astroid.nodes.BinOp):
# Il tipo è una unione di tipi, per esempio "int | float"
# print(f"[DEBUG] {ann}")
equivalent_types = []
while isinstance(ann, astroid.nodes.BinOp):
assert ann.op == "|"
equivalent_types.insert(0, ann.right)
ann = ann.left
equivalent_types.insert(0, ann)
return [build_type_annotation(ann_type) for ann_type in equivalent_types]
# --- c) tupla
elif isinstance(ann, astroid.nodes.Subscript):
# Il tipo è una composizione che coinvolge le parentesi quadre, come "List[]"
# print(f"[DEBUG] {ann}")
assert isinstance(ann.value, astroid.nodes.Name)
type_ = [ann.value.name]
print(f"\n\n{type_}\n\n")
type_built = build_type_annotation(ann.slice)
if isinstance(type_built, str):
type_params = [type_built]
else:
assert isinstance(type_built, list) or isinstance(type_built, tuple)
type_params = list(type_built)
return tuple(type_ + type_params)
elif isinstance(ann, astroid.nodes.Tuple):
# Il tipo è una composizione che coinvolge un elenco separato da virgole, per esempio "[int, float]"
# print(f"[DEBUG] {ann}")
assert isinstance(ann.elts, list)
return [build_type_annotation(e) for e in ann.elts]
else:
assert False
def visit(node):
do_something(node)
for child in node.get_children():
visit(child)
def do_something(node):
if isinstance(node, astroid.nodes.FunctionDef):
for ann in node.args.annotations:
print(f"[DEBUG] Annotazione: {build_type_annotation(ann)}")
visit(tree)
[DEBUG] Annotazione: None [DEBUG] Annotazione: fake_alias.MyType.NestedMyType ['Tuple'] ['List'] ['Tuple'] ['List'] [DEBUG] Annotazione: [('Tuple', ('List', 'str'), [('Tuple', 'int', 'int'), 'float', 'Exception']), 'int', ('List', 'str', 'int')] [DEBUG] Annotazione: fake_alias.AliasMyType [DEBUG] Annotazione: None
def lookup_type_name(node, name):
assert isinstance(name, str) and len(name) > 0
base = name.split(".")[0]
tail = ".".join(name.split(".")[1:])
return track_type_name(node, base, tail)
def track_type_name(node, base, tail):
# Cerchiamo a partire dal nodo dove è stato definito il nome base per cercare il tipo che deriva
# da esso (tail) o che rappresenta direttamente (se tail è assente)
assert base is not None
# print(
# f"[DEBUG] Cercando il nome {base}{' da cui origina ' + tail + ' ' if tail else ' '}a partire "
# f"dal nodo di tipo {type(node).__name__} e nome {node.name}")
# Cerca da dove arriva il nome del tipo partendo da questo nodo
_, type_matches = node.lookup(base)
# print(f"[DEBUG] {type_matches}")
if type_matches == () or len(type_matches) > 1:
# Nessun match o match multipli, quindi non si sa esattamente a che tipo si riferisca il nome.
# Interrompi subito!
return None
type_match_node = type_matches[0]
# print(f"[DEBUG] Match del tipo {type(type_match_node).__type__}")
# Controlla il tipo del match per continuare il tracking del tipo
if isinstance(type_match_node, astroid.nodes.Import) or \
isinstance(type_match_node, astroid.nodes.ImportFrom):
return handle_type_match_Imports(type_match_node, base, tail)
elif isinstance(type_match_node, astroid.nodes.ClassDef):
return handle_type_match_ClassDef(type_match_node, base, tail)
elif isinstance(type_match_node, astroid.nodes.AssignName):
return handle_type_match_AssignName(type_match_node, base, tail)
# Il match ottenuto è su un tipo di nodo non previsto per il tracking
return None
def handle_type_match_Imports(match_node, base, tail):
# Abbiamo uno statement del tipo:
# a) >>> import name_1, name_2 as alias_2, name_3, ...
# b) >>> from mod_or_pkg import name_1, name_2 as alias_2, name_3, ...
# La nostra base ha un match con uno dei name o alias presenti nei suddetti statement.
# Nel caso a) i name sono tutti moduli o packages e stiamo cercando un tipo, ci deve essere
# una tail che lo specifica. Nel caso b) stiamo importando da un mod_or_pkg ed i name potrebbero
# anche già essere delle classi
if isinstance(match_node, astroid.nodes.Import):
assert tail is not None
matched_mod_name = None
new_base = new_tail = None
if isinstance(match_node, astroid.nodes.Import):
# Nel caso a) possiamo importare più packages/moduli con possibile alias
for mod_name, mod_alias in reversed(match_node.names):
# Cerchiamo con quale alias o nome coincide la base
mod_to_cmp = mod_alias if mod_alias else mod_name
if base == mod_to_cmp:
matched_mod_name = mod_name
new_base = tail.split(".")[0]
new_tail = ".".join(tail.split(".")[1:])
break
elif isinstance(match_node, astroid.nodes.ImportFrom):
# Nel caso b) abbiamo un package/modulo fisso da cui possiamo importare tutti (*) o più nomi
# (rappresentanti packages/moduli/classi/variabili)
matched_mod_name = match_node.modname
# Caso di un wildcard from import
if len(match_node.names) == 1 and match_node.names[0] == ("*", None):
# Continuare direttamente nel modulo da cui stiamo importando tutto
new_base = base
new_tail = tail
# Normale from import
else:
# Cerchiamo con quale alias o nome coincide la base
for name, alias in reversed(match_node.names):
name_to_cmp = alias if alias else name
if base == name_to_cmp:
new_base = name
new_tail = tail
break
else:
assert False
# Lookup ha trovato un match, quindi qualcosa da seguire ci deve essere
assert matched_mod_name is not None and new_base is not None
# Abbiamo quindi un package/modulo da cui partire (matched_mod_name) a cui può seguire una
# sequenza di altri packages/moduli/classi che contengono il nostro tipo di interesse,
# contenuto nella fine di tail. Quello che dobbiamo fare è trovare l'ultimo package/modulo
# che definisce la prima classe.
#
# Per esempio:
# - matched_mod_name è il package 'pkg_1';
# - new_base è il package 'pkg_2';
# - new_tail è la sequenza di modulo 'mod_1', classe 'cls_1' e classe innestata finale 'cls_2'.
# Il nome completo è 'pkg_1.pkg_2.mod_1.cls_1.cls_2'.
# |-> Questo è quello che vogliamo trovare e importare!
# Per trovarlo partiamo dalla fine in reverse finché non troviamo un modulo importabile.
complete_type_name = ".".join([matched_mod_name, new_base] + ([new_tail] if new_tail else []))
complete_type_name_list = complete_type_name.split(".")
to_follow_ast = None
i = 1
while i < len(complete_type_name_list):
to_import_mod = ".".join(complete_type_name_list[:-i])
try:
to_follow_ast = match_node.do_import_module(to_import_mod)
break
except Exception:
i += 1
if to_follow_ast:
j = len(complete_type_name_list) - i
new_base = ".".join(complete_type_name_list[j:j+1])
new_tail = ".".join(complete_type_name_list[j+1:])
return track_type_name(to_follow_ast, new_base, new_tail)
return None
def handle_type_match_ClassDef(match_node, base, tail):
if tail:
# Se c'è una coda a seguire, ci sono classi innestate! Controlliamole ricorsivamente.
new_base = tail.split(".")[0]
new_tail = ".".join(tail.split(".")[1:])
return track_type_name(match_node, new_base, new_tail)
else:
# Se non c'è coda siamo arrivati ad una definizione di classe coincidente col nostro tipo.
# SUCCESSO!
assert match_node.name == base
# Ricostruisci il nome completo del tipo, perso nei vari lookup, risalendo gli scopes
type_name = base
complete_type_name = [base]
scope_node = match_node.parent.scope()
while type(scope_node) is astroid.nodes.ClassDef:
complete_type_name.append(scope_node.name)
scope_node = scope_node.parent.scope()
complete_type_name = ".".join(complete_type_name)
print(f"[DEBUG] Tipo {type_name} ({complete_type_name}) definito nel modulo {match_node.root().name}")
return match_node
def handle_type_match_AssignName(match_node, base, tail):
# Abbiamo un assegnamento crea un alias del nostro tipo
assert isinstance(match_node.parent, astroid.nodes.Assign)
# Proviamo a risalire alla vera definizione tramite l'inferenza di astroid, possibile in un
# nodo di tipo astroid.nodes.AssignName
infers = list(match_node.infer())
if len(infers) == 1 and isinstance(infers[0], astroid.nodes.ClassDef):
# Solo se abbiamo un unico risultato certo che punta ad una classe
inferred = infers[0]
# Ricostruisci il nome completo del tipo, perso nei vari lookup, risalendo gli scopes
type_name = inferred.name
complete_type_name = [inferred.name]
scope_node = inferred.parent.scope()
while type(scope_node) is astroid.nodes.ClassDef:
complete_type_name.append(scope_node.name)
scope_node = scope_node.parent.scope()
complete_type_name = ".".join(complete_type_name)
print(f"[DEBUG] Tipo {type_name} ({complete_type_name}) definito nel modulo {match_node.root().name}")
return inferred
return None
def visit(node):
do_something(node)
for child in node.get_children():
visit(child)
def do_something(node):
if isinstance(node, astroid.nodes.Arguments):
print(node)
visit(tree)
Arguments(vararg=None, kwarg=None, args=[ <AssignName.w l.6 at 0x172212c5160>, <AssignName.x l.6 at 0x172212c5910>, <AssignName.y l.6 at 0x172212c5df0>, <AssignName.z l.6 at 0x172212c5f40>], defaults=[], kwonlyargs=[], posonlyargs=[], posonlyargs_annotations=[], kw_defaults=[], annotations=[ None, <Attribute.NestedMyType l.6 at 0x172212c5550>, <BinOp l.6 at 0x172212c5640>, <Attribute.AliasMyType l.6 at 0x172212e67f0>], varargannotation=None, kwargannotation=None, kwonlyargs_annotations=[], type_comment_args=[None, None, None, None], type_comment_kwonlyargs=[], type_comment_posonlyargs=[]) Arguments(vararg='var_args', kwarg='kw_args', args=[<AssignName.normal_1 l.11 at 0x172212c65e0>], defaults=[<Const.int l.11 at 0x172212c6100>], kwonlyargs=[<AssignName.kw_only_1 l.11 at 0x172212c6130>], posonlyargs=[<AssignName.pos_only_1 l.11 at 0x172212c6a90>], posonlyargs_annotations=[None], kw_defaults=[<Const.int l.11 at 0x172212c67c0>], annotations=[None], varargannotation=None, kwargannotation=None, kwonlyargs_annotations=[None], type_comment_args=[None], type_comment_kwonlyargs=[None], type_comment_posonlyargs=[None])
def visit(node):
do_something(node)
for child in node.get_children():
visit(child)
def do_something(node):
if isinstance(node, astroid.nodes.FunctionDef):
for ann in node.args.annotations:
built_type = build_type_annotation(ann)
if built_type:
if isinstance(built_type, str):
r = lookup_type_name(node, built_type)
else:
b = built_type
while isinstance(b, list) or isinstance(b, tuple):
b = b[0]
r = lookup_type_name(node, b)
print(f"[DEBUG] {r}")
print("-"*MAX_LEN)
print("-"*MAX_LEN)
visit(tree)
----------------------------------------------------------------------------------------------------------------------------- [DEBUG] Tipo NestedMyType (NestedMyType.MyType) definito nel modulo params_pkg.params_nested_pkg.params_md_3 [DEBUG] ClassDef.NestedMyType(name='NestedMyType', doc=None, is_dataclass=False, position=Position(lineno=6, col_offset=4, end_lineno=6, end_col_offset=22), decorators=None, bases=[], keywords=[], doc_node=None, body=[<FunctionDef.__init__ l.7 at 0x172212e6310>]) ----------------------------------------------------------------------------------------------------------------------------- ['Tuple'] ['List'] ['Tuple'] ['List'] [DEBUG] Tipo Tuple (Tuple) definito nel modulo typing [DEBUG] ClassDef.Tuple(name='Tuple', doc=None, is_dataclass=False, position=None, decorators=None, bases=[<ClassDef.tuple l.0 at 0x1722474d250>], keywords=[], doc_node=None, body=[]) ----------------------------------------------------------------------------------------------------------------------------- [DEBUG] Tipo MyType (MyType) definito nel modulo params_pkg.params_md_2 [DEBUG] ClassDef.MyType(name='MyType', doc=None, is_dataclass=False, position=Position(lineno=2, col_offset=0, end_lineno=2, end_col_offset=12), decorators=None, bases=[], keywords=[], doc_node=None, body=[ <FunctionDef.__init__ l.3 at 0x172212e6a00>, <ClassDef.NestedMyType l.6 at 0x172212e6c40>]) -----------------------------------------------------------------------------------------------------------------------------
path_1 = "import_md.py"
name_1 = os.path.splitext(os.path.split(path_1)[1])[0]
with open(path_1, "r", encoding="utf8") as f1:
code_text_1 = f1.read()
t = '\n'.join([S+line for line in code_text_1.split('\n')]); print(f"Modulo 1:\n{t}")
tree = astroid.parse(code_text_1, path=path_1, module_name=name_1)
Modulo 1: import params_pkg.params_md_2 _ = params_md_2.MyType() import params_pkg.params_md_3, params_pkg.params_md_2 _ = params_md_2.MyType() import params_pkg.params_md_2.MyType _ = MyType() import params_pkg.params_md_2.MyType as AliasMyType _ = AliasMyType() from params_pkg.params_md_2 import * _ = MyType() from params_pkg.params_md_2 import MyType _ = MyType() from params_pkg.params_md_2 import MyType as AliasMyType _ = AliasMyType()
def visit(node):
do_something(node)
for child in node.get_children():
visit(child)
def do_something(node):
if isinstance(node, astroid.nodes.Import) or isinstance(node, astroid.nodes.ImportFrom):
print(node)
visit(tree)
Import(names=[('params_pkg.params_md_2', None)]) Import(names=[('params_pkg.params_md_3', None), ('params_pkg.params_md_2', None)]) Import(names=[('params_pkg.params_md_2.MyType', None)]) Import(names=[('params_pkg.params_md_2.MyType', 'AliasMyType')]) ImportFrom(modname='params_pkg.params_md_2', names=[('*', None)], level=None) ImportFrom(modname='params_pkg.params_md_2', names=[('MyType', None)], level=None) ImportFrom(modname='params_pkg.params_md_2', names=[('MyType', 'AliasMyType')], level=None)
path_1 = "type_infer_md.py"
name_1 = os.path.splitext(os.path.split(path_1)[1])[0]
with open(path_1, "r", encoding="utf8") as f1:
code_text_1 = f1.read()
t = '\n'.join([S+line for line in code_text_1.split('\n')]); print(f"Modulo 1:\n{t}")
tree = astroid.parse(code_text_1, path=path_1, module_name=name_1)
Modulo 1: class C: pass def foo(a): return str(a) def g(): return f() def h(): c = C() a = 1 + 0.5 return c 1 + 3.5 'A' + foo(1) foo(b) g() h()
def visit(node):
do_something(node)
for child in node.get_children():
visit(child)
def do_something(node):
if isinstance(node, astroid.nodes.Expr):
print(f"[DEBUG] {node}")
print(f"[DEBUG] {node.value}")
try:
infers = node.value.inferred()
except Exception:
infers = []
assert isinstance(infers, list)
if len(infers) == 1: # Solo risposte certe
inferred_value = infers[0]
if inferred_value is not astroid.Uninferable:
assert getattr(inferred_value, "pytype", None) is not None
complete_inferred_type = inferred_value.pytype()
assert "." in complete_inferred_type
assert complete_inferred_type.startswith(f"builtins.") or \
complete_inferred_type.startswith(f"{node.root().name}.") # node.root().name potrebbe essere vuoto
inferred_type = ".".join(complete_inferred_type.split(".")[1:])
print(f"[DEBUG] {inferred_type} ({type(inferred_type)})")
scope = node.scope()
assert scope
lookup_type_name(scope, inferred_type)
else:
print(f"[DEBUG] Impossibile risalire al tipo")
else:
print(f"[DEBUG] Impossibile risalire al tipo")
print("-"*MAX_LEN)
print("-"*MAX_LEN)
visit(tree)
----------------------------------------------------------------------------------------------------------------------------- [DEBUG] Expr(value=<BinOp l.20 at 0x17224fb9790>) [DEBUG] BinOp(op='+', left=<Const.int l.20 at 0x17225258be0>, right=<Const.float l.20 at 0x172212dbe20>) [DEBUG] float (<class 'str'>) [DEBUG] Tipo float (float) definito nel modulo builtins ----------------------------------------------------------------------------------------------------------------------------- [DEBUG] Expr(value=<BinOp l.22 at 0x172212db700>) [DEBUG] BinOp(op='+', left=<Const.str l.22 at 0x172212db8b0>, right=<Call l.22 at 0x172212db8e0>) [DEBUG] str (<class 'str'>) [DEBUG] Tipo str (str) definito nel modulo builtins ----------------------------------------------------------------------------------------------------------------------------- [DEBUG] Expr(value=<Call l.24 at 0x172212db6a0>) [DEBUG] Call(func=<Name.foo l.24 at 0x172212db910>, args=[<Name.b l.24 at 0x172212db6d0>], keywords=[]) [DEBUG] str (<class 'str'>) [DEBUG] Tipo str (str) definito nel modulo builtins ----------------------------------------------------------------------------------------------------------------------------- [DEBUG] Expr(value=<Call l.26 at 0x172212db850>) [DEBUG] Call(func=<Name.g l.26 at 0x172212db880>, args=[], keywords=[]) [DEBUG] Impossibile risalire al tipo ----------------------------------------------------------------------------------------------------------------------------- [DEBUG] Expr(value=<Call l.28 at 0x172212dbdc0>) [DEBUG] Call(func=<Name.h l.28 at 0x172212db760>, args=[], keywords=[]) [DEBUG] C (<class 'str'>) [DEBUG] Tipo C (C) definito nel modulo type_infer_md -----------------------------------------------------------------------------------------------------------------------------
path_1 = "cls_fields_md.py"
name_1 = os.path.splitext(os.path.split(path_1)[1])[0]
with open(path_1, "r", encoding="utf8") as f1:
code_text_1 = f1.read()
t = '\n'.join([S+line for line in code_text_1.split('\n')]); print(f"Modulo 1:\n{t}")
tree = astroid.parse(code_text_1, path=path_1, module_name=name_1)
Modulo 1: e = 1 f = 2 def foo(): return "Valore di 'f' in 'C2' fuori dall'init" class C1: a: str j = "Valore di 'j' in 'C1' fuori dall'init" k = "Valore di 'k' in 'C1' fuori dall'init" def __init__(self, param: int): self.a = f"Valore di 'a' in 'C1' derivante dal parametro {param} dell'init" class C2: f = foo() global e e += 1 h = b, e = ("Valore di 'b' in 'C2' fuori dall'init", "Valore di 'e' in 'C2' fuori dall'init") def __init__(self): self.a = "Valore di 'a' in 'C2' dentro l'init" self.b: str = "Valore di 'b' in 'C2' dentro l'init" c = "Non sono nulla" def __str__(self): return "Valore di 'g' in 'C4' fuori dall'init" class C3(C1): i = "Valore di 'i' in 'C3' fuori dall'init" l = "Valore di 'l' in 'C3' fuori dall'init" def __init__(self, param: int = 1, /): self.a = "Valore di 'a' in 'C3' prima di chiamare super dentro l'init" super() super().__init__(param) # self.a = "Valore di 'a' in 'C3' dopo aver chiamato super dentro l'init" self.c = "Valore di 'c' in 'C3' dentro l'init" class C4(C2, C3): d = "Valore di 'd' in 'C4' fuori dall'init" g = C2() k = "Valore di 'k' in 'C4' fuori dall'init" c = C4() attrs = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"] # fino a l for attr in attrs: attr_v = getattr(c, attr, None) if attr_v: print(f"Attributo {attr} presente nell'istanza della classe {type(c).__name__} affermante: {attr_v}") else: print(f"Attributo {attr} assente nell'istanza della classe {type(c).__name__}")
Tutti gli AnnAssign nel corpo della classe sono potenziali attributi, ma gli attributi veri sono gli assegnamenti in __init__. Cio è non è assegnato in __init__ non vede mai la luce. Tuttavia capita che gli AnnAssign siano usati per specificare il tipo, quindi quello che troviamo dentro __init__ lo cerchiamo eventualmente tra gli AnnAssign per cercare il tipo del campo, altrimenti proviamo ad indurlo dagli assegnamenti dei parametri. Si ricordi che se una classe eredita da una o più classi può ereditare anche i loro attributi (se il costruttore è esplicito, solo se esegue correttamente la chiamata a super).
def is_static_method(node):
assert isinstance(node, astroid.nodes.FunctionDef)
decorators_nodes = node.decorators.nodes if node.decorators else []
decorators_names = [node.name for node in decorators_nodes]
if "staticmethod" in decorators_names:
return True
else:
return False
def get_self_ref(node):
# I parametri in ordine sono:
# - posonlyargs
# - args
# - vararg
# - kwonlyargs
# - kwarg
assert isinstance(node, astroid.nodes.FunctionDef)
if is_static_method(node):
return None
else:
if node.args.posonlyargs:
return node.args.posonlyargs[0].name
else:
return node.args.args[0].name
def get_anv_list_class(node: astroid.nodes.ClassDef):
# --- 1 Tutti i campi definiti con assegnamento (non annotazione) nel corpo della classe
assert isinstance(node, astroid.nodes.ClassDef)
# Trova i nomi che si riferiscono a variabili esterne alla classe
# (definite da Global, dato che Nonlocal non ha senso in una classe)
global_names = set()
for class_body_node in node.get_children():
if isinstance(class_body_node, astroid.nodes.Global):
for name in class_body_node.names:
global_names.add(name)
# Trova i nomi che si riferiscono a campi e potenziali campi della classe dal suo corpo
for class_body_node in node.get_children():
# print(f"[DEBUG] {class_body_node}")
if isinstance(class_body_node, astroid.nodes.Assign):
for target in class_body_node.targets: # Assegnamenti a catena possibili
if isinstance(target, astroid.nodes.Tuple):
for e in target.elts:
assert isinstance(e, astroid.nodes.AssignName) or isinstance(e, astroid.nodes.AssignAttr)
names = []
for e in target.elts:
if isinstance(e, astroid.nodes.AssignName) and e.name not in global_names:
names.append(e.name)
else:
assert isinstance(e, astroid.nodes.AssignAttr) or e.name in global_names
names.append(None)
for name in names:
if name is not None:
# Restituisci solo se almeno un nome è None, se sono tutti None (attributi o globali)
# non restituirla neanche
yield (None, names, class_body_node.value, node,)
break
elif isinstance(target, astroid.nodes.AssignName):
yield (None, target.name, class_body_node.value, node,)
else:
assert isinstance(target, astroid.nodes.AssignAttr)
pass
elif isinstance(class_body_node, astroid.nodes.AnnAssign):
if isinstance(class_body_node.target, astroid.nodes.AssignName):
yield (class_body_node.annotation,
class_body_node.target.name,
class_body_node.value,
node,)
else:
assert isinstance(class_body_node.target, astroid.nodes.AssignAttr)
pass
def get_anv_list_constructor(class_node: astroid.nodes.ClassDef, constructor_node: astroid.nodes.FunctionDef):
# --- 2 Tutti i campi derivanti dal costruttore
assert isinstance(class_node, astroid.nodes.ClassDef)
assert isinstance(constructor_node, astroid.nodes.FunctionDef) and constructor_node.name == "__init__"
if not constructor_node.body or is_static_method(constructor_node):
# Se non c'è body la classe non dichiara __init__ né eredita da altre classi, avendo in mano quindi
# il costruttore vuoto di object
# Implicazione: no attributi derivabili da questo __init__!
# Note: se per qualche motivo folle il metodo __init__ è statico si cade sempre qui!
# Non vi possono essere attributi derivanti dal costruttore se non esiste un "self" a cui riferirsi
pass
elif class_node.name == constructor_node.parent.name:
# Se c'è body ed il padre del costruttore è la stessa classe, il costruttore è
# dichiarato esplicitamente nella stessa classe
# Implicazione: cercare gli attributi nel body!
# Trovare il parametro usato per riferirsi all'istanza dell'oggetto, solitamente "self" (ma non necessariamente)
self_ref = get_self_ref(constructor_node)
assert self_ref
#! super().__init__ potrebbe essere chiamato dentro un altro metodo o funzione e fare comunque il suo dovere.
# Io controllo solo se chiamato direttamente, poiché le sue potenziali chiamate innestate diventano difficili
# da tracciare e sono inverosimili.
for constructor_body_node in constructor_node.body:
# print(f"[DEBUG] {constructor_body_node}")
if isinstance(constructor_body_node, astroid.nodes.Assign):
for target in constructor_body_node.targets: # Assegnamenti a catena possibili
if isinstance(target, astroid.nodes.Tuple):
for e in target.elts:
assert isinstance(e, astroid.nodes.AssignName) or isinstance(e, astroid.nodes.AssignAttr)
if isinstance(e, astroid.nodes.AssignAttr):
assert isinstance(e.expr, astroid.nodes.Name)
names = []
for e in target.elts:
if isinstance(e, astroid.nodes.AssignAttr) and e.expr.name == self_ref:
names.append(e.attrname)
else:
assert isinstance(e, astroid.nodes.AssignName) or e.expr.name != self_ref
names.append(None)
for name in names:
if name is not None:
# Restituisci solo se almeno un nome è None, se sono tutti None (attributi o globali)
# non restituirla neanche
yield (None, names, constructor_body_node.value, constructor_body_node,)
break
elif isinstance(target, astroid.nodes.AssignAttr):
assert isinstance(target.expr, astroid.nodes.Name)
if target.expr.name == self_ref:
yield (None, target.attrname, constructor_body_node.value, constructor_body_node,)
else:
assert isinstance(target, astroid.nodes.AssignName)
pass
elif isinstance(constructor_body_node, astroid.nodes.AnnAssign):
# No tuple negli AnnAssign
if isinstance(constructor_body_node.target, astroid.nodes.AssignAttr):
assert isinstance(constructor_body_node.target.expr, astroid.nodes.Name)
if constructor_body_node.target.expr.name == self_ref:
yield (constructor_body_node.annotation,
constructor_body_node.target.attrname,
constructor_body_node.value,
constructor_body_node,)
else:
assert isinstance(constructor_body_node.target, astroid.nodes.AssignAttr)
pass
elif isinstance(constructor_body_node, astroid.nodes.Expr):
is_super_constructor_call = False
parent_constructor_node = None
is_super_constructor_call = \
isinstance(constructor_body_node.value, astroid.nodes.Call) and \
isinstance(constructor_body_node.value.func, astroid.nodes.Attribute) and \
constructor_body_node.value.func.attrname == "__init__" and \
isinstance(constructor_body_node.value.func.expr, astroid.nodes.Call) and \
isinstance(constructor_body_node.value.func.expr.func, astroid.nodes.Name) and \
constructor_body_node.value.func.expr.func.name == "super"
if is_super_constructor_call:
mro = class_node.mro()
assert len(mro) > 1 # Se si sta chiamando 'super' qualcuno da cui ereditare c'è!
first_parent_node = mro[1]
parent_constructor_node = None
for method in first_parent_node.methods():
assert isinstance(method, astroid.nodes.FunctionDef)
if method.name == "__init__":
parent_constructor_node = method
break
assert parent_constructor_node
yield from get_anv_list_constructor(first_parent_node, parent_constructor_node)
elif class_node.name != constructor_node.parent.name:
# Se c'è body ed il padre del costruttore è diverso dalla stessa classe, il costruttore è
# uno di quelli delle classi da cui eredita.
# Implicazione: cercare gli attributi dai genitori!
pass
else:
assert False
def get_anv_list(node: astroid.nodes.ClassDef): # Abbr. di annotation_name_value_list, rinominarlo in atv
assert isinstance(node, astroid.nodes.ClassDef)
# print(f"[DEBUG] Sono {node.name}")
# A prescindere una classe prende, esattamente in questo ordine (per i tipi eventuali):
# 1 Tutti i campi definiti con assegnamento (non annotazione) nel corpo della classe;
# 2a Se non definisce esplicitamente un __init__, tutti i campi definiti dal primo __init__
# ereditato secondo il MRO (method resolution order)
# 2b Se definisce esplicitamente un __init__, tutti i campi definiti in esso, e se vi è una
# chiamata a super() anche quelli dell'__init__ ereditato secondo il MRO
# Per i tipi facciamo che se è stata lasciata l'annotazione nel corpo della classe li mettiamo,
# se il metodo 'infer' ci da un valore di cui possiamo ottenere il tipo lo mettiamo, altrimenti
# ciao buona perché non posso impazzire!
# --- 1 Tutti i campi definiti con assegnamento (non annotazione) nel corpo di questa classe e nel corpo
# di tutti i predecessori di ogni grado. Quelli nei genitori vengono sovrascritti però, quindi parti
# prima dai predecessori più vecchi fino ai più recenti
reversed_ancestors_list = reversed(list(node.ancestors()))
for ancestor in reversed_ancestors_list:
yield from get_anv_list_class(ancestor)
yield from get_anv_list_class(node)
# --- 2 Tutti i campi derivanti dal costruttore di questa classe
# Trova il nodo associato al costruttore
constructor_node = None
for method_node in node.methods():
if method_node.name =="__init__":
constructor_node = method_node
break
# Un costruttore c'è sempre, anche se non è stato dichiarato esplicitamente
assert constructor_node and isinstance(constructor_node, astroid.nodes.FunctionDef)
# print(f"[DEBUG] {constructor_node.args}")
yield from get_anv_list_constructor(node, constructor_node)
def get_fields(node: astroid.nodes.ClassDef):
assert isinstance(node, astroid.nodes.ClassDef)
fields_dict = dict()
for annotation, target, value, def_node in get_anv_list(node):
if isinstance(target, list):
# Gli assegnamenti multipli (di tuple) sono possibili solo con Assign, non AnnAssign,
# quindi l'annotazione è assente
assert annotation is None
# Il tipo lo si può ottenere solo da inferenza, che per ora ignoriamo!
inferred_annotation = None
for name in target:
prev_annotation, prev_value, prev_def_node = fields_dict.get(name, (None, None, None,))
# Anche se c'è una annotazione precedente, vale l'ultima
new_annotation = None # Chiamare qui check_annotation sull'annotazione
else:
assert isinstance(target, str)
def visit(node):
do_something(node)
for child in node.get_children():
visit(child)
def do_something(node):
if isinstance(node, astroid.nodes.ClassDef):
get_fields(node)
visit(tree)
I commenti possono contenere informazioni aggiuntive su moduli, classi e metodi, indicando una descrizione loro e dei loro campi/parametri, oltre che dei tipi coninvolti (nel caso non presenti nelle annotazioni).
path_1 = "docstring_md_2.py"
name_1 = os.path.splitext(os.path.split(path_1)[1])[0]
with open(path_1, "r", encoding="utf8") as f1:
code_text_1 = f1.read()
t = '\n'.join([S+line for line in code_text_1.split('\n')]); print(f"Modulo 1:\n{t}")
tree = astroid.parse(code_text_1, path=path_1, module_name=name_1)
Modulo 1: # Epydoc or Epytext def fun_epydoc(pos_only_1, /, normal_1: int = 0, *var_args, kw_only_1: str = "", **kw_args) -> bool: """Description of the method @param pos_only_1: a positional only parameter @type normal_1: int @param normal_1: a normal (identifiable through position or keyword) parameter @param var_args: various arbitrary positional parameters @type kw_only_1: str @param kw_only_1: a keyword only parameter of known string type @param kw_args: various arbitrary keyword parameters @rtype: bool @return: description of the returned object """ pass # reST def fun_rest(pos_only_1, /, normal_1: int = 0, *var_args, kw_only_1: str = "", **kw_args) -> bool: """Description of the method :param pos_only_1: a positional only parameter :type normal_1: int :param normal_1: a normal (identifiable through position or keyword) parameter :param var_args: various arbitrary positional parameters :type kw_only_1: str :param kw_only_1: a keyword only parameter of known string type :param kw_args: various arbitrary keyword parameters :rtype: bool :return: description of the returned object """ pass # Google def fun_google(pos_only_1, /, normal_1: int = 0, *var_args, kw_only_1: str = "", **kw_args) -> bool: """Description of the method Args: pos_only_1: a positional only parameter normal_1 (int): a normal (identifiable through position or keyword) parameter *var_args: various arbitrary positional parameters kw_only_1 (str): a keyword only parameter **kw_args: various arbitrary keyword parameters Returns: bool: description of the returned object """ pass # Numpy def fun_numpy(pos_only_1, /, normal_1: int = 0, *var_args, kw_only_1: str = "", **kw_args) -> bool: """Description of the method Parameters ---------- pos_only_1 a positional only parameter normal_1 : int a normal (identifiable through position or keyword) parameter var_args various arbitrary positional parameters kw_only_1 : str a keyword only parameter kw_args various arbitrary keyword parameters Returns ------- bool description of the returned object """ pass
styles = (dp.DocstringStyle.EPYDOC, dp.DocstringStyle.REST, dp.DocstringStyle.GOOGLE, dp.DocstringStyle.NUMPYDOC)
for i, fun_node in enumerate(tree.get_children()):
str_comment = fun_node.doc
docstring = dp.parse(str_comment,)
assert docstring.style is styles[i]
print(docstring.short_description)
print(f"Lo stile della stringa #{i+1} è correttamente {docstring.style}")
print(f"I parametri sono:")
for i, param in enumerate(docstring.params):
print(f"{S}{i}\n"
f"{S}args: {param.args}\n"
f"{S}description: {param.description}\n"
f"{S}type name: {param.type_name}\n"
f"{S}optional: {param.is_optional}\n"
f"{S}default: {param.default}\n")
Description of the method Lo stile della stringa #1 è correttamente DocstringStyle.EPYDOC I parametri sono: 0 args: ['param', 'pos_only_1'] description: a positional only parameter type name: None optional: False default: None 1 args: ['type', 'normal_1'] description: a normal (identifiable through position or keyword) parameter type name: int optional: False default: None 2 args: ['param', 'var_args'] description: various arbitrary positional parameters type name: None optional: False default: None 3 args: ['type', 'kw_only_1'] description: a keyword only parameter of known string type type name: str optional: False default: None 4 args: ['param', 'kw_args'] description: various arbitrary keyword parameters type name: None optional: False default: None Description of the method Lo stile della stringa #2 è correttamente DocstringStyle.REST I parametri sono: 0 args: ['param', 'pos_only_1'] description: a positional only parameter type name: None optional: None default: None 1 args: ['param', 'normal_1'] description: a normal (identifiable through position or keyword) parameter type name: int optional: None default: None 2 args: ['param', 'var_args'] description: various arbitrary positional parameters type name: None optional: None default: None 3 args: ['param', 'kw_only_1'] description: a keyword only parameter of known string type type name: str optional: None default: None 4 args: ['param', 'kw_args'] description: various arbitrary keyword parameters type name: None optional: None default: None Description of the method Lo stile della stringa #3 è correttamente DocstringStyle.GOOGLE I parametri sono: 0 args: ['param', 'pos_only_1'] description: a positional only parameter type name: None optional: None default: None 1 args: ['param', 'normal_1 (int)'] description: a normal (identifiable through position or keyword) parameter type name: int optional: False default: None 2 args: ['param', '*var_args'] description: various arbitrary positional parameters type name: None optional: None default: None 3 args: ['param', 'kw_only_1 (str)'] description: a keyword only parameter type name: str optional: False default: None 4 args: ['param', '**kw_args'] description: various arbitrary keyword parameters type name: None optional: None default: None Description of the method Lo stile della stringa #4 è correttamente DocstringStyle.NUMPYDOC I parametri sono: 0 args: ['param', 'pos_only_1'] description: a positional only parameter type name: None optional: None default: None 1 args: ['param', 'normal_1'] description: a normal (identifiable through position or keyword) parameter type name: int optional: False default: None 2 args: ['param', 'var_args'] description: various arbitrary positional parameters type name: None optional: None default: None 3 args: ['param', 'kw_only_1'] description: a keyword only parameter type name: str optional: False default: None 4 args: ['param', 'kw_args'] description: various arbitrary keyword parameters type name: None optional: None default: None
Astroid può leggere tutto quello che è effettivamente installato. Per analizzare un progetto basta installarlo e poi eseguire astroid nello stesso ambiente virtuale. Quando un modulo richiamerà altri moduli, astroid troverà anche quei moduli e potrà parsarli e analizzarli a sua volta, se presenti e raggiungibili.
I progetti da analizzare potranno fare ricorso a librerie che non sono presenti tra le dipendenze di CodeOntology, e che quindi non possono essere individuate dal modulo importlib verosimilmente usato da astroid quando si trova a che fare con degli import statement. Si potrebbe pensare di installare nello stesso ambiente in cui gira CodeOntology le dipendenze dei progetti da analizzare per risolvere la questione: questa non è però una situazione ideale.
L'idea è invece di preparare (o far preparare a CodeOntology) un secondo ambiente virtuale con le dipendenze del progetto da analizzare, e aggiungere poi tale ambiente virtuale nel path di ricerca dei moduli solo durante la fase di parsing.
Si è preparato un progetto esempio prj con un solo modulo outer_md.py che fa riferimento ad un nome dichiarato nella libreria sphinx, non è installata.
try:
import sphinx
print("Importato")
del sphinx
except Exception as e:
print(e)
Importato
Dentro prj è presente un ambiente virtuale con sphinx in cui astroid può cercarlo. Si aggiunge tale ambiente virtuale al path di ricerca. Lo si aggiunge in prima posizione per preferirlo, nel caso tale dipendenza sia presente anche in CodeOntology. Se sphinx venisse installato anche nell'ambiente virtuale che fa girare questo notebook, astroid il match lo troverebbe comunque nel venv del progetto. Questo potrebbe causare qualche problema con le dipendenze di astroid, nel caso ce ne fossero anche nel venv aggiunto come preferenziale ma di diversa versione, magari incompatibile. Verificare!
sys.path.insert(0, os.path.abspath("prj\\venv\\lib\\site-packages"))
sys.path.insert(1, os.path.abspath("prj\\venv\\lib\\site-packages"))
sys.path.insert(2, os.path.abspath("prj\\venv\\lib\\site-packages\\win32\\lib"))
sys.path.insert(3, os.path.abspath("prj\\venv\\lib\\site-packages\\Pythonwin"))
sys.path
['D:\\Coding\\Jupyter\\Py39\\ASTROID\\prj\\venv\\lib\\site-packages', 'D:\\Coding\\Jupyter\\Py39\\ASTROID\\prj\\venv\\lib\\site-packages', 'D:\\Coding\\Jupyter\\Py39\\ASTROID\\prj\\venv\\lib\\site-packages\\win32\\lib', 'D:\\Coding\\Jupyter\\Py39\\ASTROID\\prj\\venv\\lib\\site-packages\\Pythonwin', 'D:\\Coding\\Jupyter\\Py39\\ASTROID', 'C:\\Program Files\\Python39\\python39.zip', 'C:\\Program Files\\Python39\\DLLs', 'C:\\Program Files\\Python39\\lib', 'C:\\Program Files\\Python39', 'd:\\coding\\jupyter\\py39\\venv', '', 'd:\\coding\\jupyter\\py39\\venv\\lib\\site-packages', 'd:\\coding\\jupyter\\py39\\venv\\lib\\site-packages\\win32', 'd:\\coding\\jupyter\\py39\\venv\\lib\\site-packages\\win32\\lib', 'd:\\coding\\jupyter\\py39\\venv\\lib\\site-packages\\Pythonwin']
astroid.modutils.sys.path
['D:\\Coding\\Jupyter\\Py39\\ASTROID\\prj\\venv\\lib\\site-packages', 'D:\\Coding\\Jupyter\\Py39\\ASTROID\\prj\\venv\\lib\\site-packages', 'D:\\Coding\\Jupyter\\Py39\\ASTROID\\prj\\venv\\lib\\site-packages\\win32\\lib', 'D:\\Coding\\Jupyter\\Py39\\ASTROID\\prj\\venv\\lib\\site-packages\\Pythonwin', 'D:\\Coding\\Jupyter\\Py39\\ASTROID', 'C:\\Program Files\\Python39\\python39.zip', 'C:\\Program Files\\Python39\\DLLs', 'C:\\Program Files\\Python39\\lib', 'C:\\Program Files\\Python39', 'd:\\coding\\jupyter\\py39\\venv', '', 'd:\\coding\\jupyter\\py39\\venv\\lib\\site-packages', 'd:\\coding\\jupyter\\py39\\venv\\lib\\site-packages\\win32', 'd:\\coding\\jupyter\\py39\\venv\\lib\\site-packages\\win32\\lib', 'd:\\coding\\jupyter\\py39\\venv\\lib\\site-packages\\Pythonwin']
Prima di fare il parsing si è scoperto necessario un lavoro di pulizia di memoria sulle cache di astroid. astroid ha un suo sistema di cache con info sui moduli che ha già provato a importare. Per qualche motivo a questo punto potrebbero esserci informazioni su sphinx, e le informazioni sono proprio un'eccezione per impossibilità import che verrà lanciata quando si proverà ad accedere a sphinx da astroid durante la visita dell'AST.
La presenza di tale valore in cache impedisce l'accesso all'effettivamente ora disponibile sphinx! Ma per aggirarlo basta far dimenticare ad astroid il passato. Non si può efficacemente fare un reimport per ottenere un reset di astroid (importlib.reload non risolve), né la funzione clear_cache del manager di astroid pare funzionare al nostro scopo.
g = astroid.astroid_manager.MANAGER._mod_file_cache.get(("sphinx", None), None)
if g:
print(g)
importlib.reload(astroid)
g = astroid.astroid_manager.MANAGER._mod_file_cache.get(("sphinx", None), None)
if g:
print("Il 'reload' ha funzionato.")
else:
print("Il 'reload' non ha funzionato.")
astroid.astroid_manager.MANAGER.clear_cache()
g = astroid.astroid_manager.MANAGER._mod_file_cache.get(("sphinx", None), None)
if g:
print("Il 'clear_cache' ha funzionato.")
else:
print("Il 'clear_cache' non ha funzionato.")
else:
print("Questa volta non è stato trovato nulla salvato su 'Sphinx'.")
Questa volta non è stato trovato nulla salvato su 'Sphinx'.
Per questo motivo mi trovo costretto ad un'operazione di pulizia manuale! Bisogna stare però attenti a non rimuovere tutto tutto, poiché altrimenti perdiamo i builtins o altre cose importanti. La tecnica è, appenda si fa l'import di astroid, di fare un backup delle cache e ripristinarlo ogni volta che si vuole fare pulizia.
def astroid_brain_wash():
astroid.astroid_manager.MANAGER._mod_file_cache = ASTROID_CACHE_BACKUP.copy()
astroid_brain_wash()
try:
astroid.astroid_manager.MANAGER._mod_file_cache[("sphinx", None)]
except KeyError:
print("Dimenticato tutto!")
Dimenticato tutto!
A questo punto facciamo il parsing.
path_1 = "prj/outer_md.py"
name_1 = os.path.splitext(os.path.split(path_1)[1])[0]
with open(path_1, "r", encoding="utf8") as f1:
code_text_1 = f1.read()
t = '\n'.join([S+line for line in code_text_1.split('\n')]); print(f"Modulo 1:\n{t}")
tree = astroid.parse(code_text_1, path=path_1, module_name=name_1)
Modulo 1: from sphinx import * a: RemovedInNextVersionWarning() = None
Rimuoviamo l'ambiente virtuale del progetto dal path di ricerca.
sys.path = sys.path[4:]
sys.path
['D:\\Coding\\Jupyter\\Py39\\ASTROID', 'C:\\Program Files\\Python39\\python39.zip', 'C:\\Program Files\\Python39\\DLLs', 'C:\\Program Files\\Python39\\lib', 'C:\\Program Files\\Python39', 'd:\\coding\\jupyter\\py39\\venv', '', 'd:\\coding\\jupyter\\py39\\venv\\lib\\site-packages', 'd:\\coding\\jupyter\\py39\\venv\\lib\\site-packages\\win32', 'd:\\coding\\jupyter\\py39\\venv\\lib\\site-packages\\win32\\lib', 'd:\\coding\\jupyter\\py39\\venv\\lib\\site-packages\\Pythonwin']
Si vede che sphinx non lo si riesce infatti a importare.
try:
import sphinx
print("Importato")
del sphinx
except Exception as e:
print(e)
Importato
Tuttavia dai nodi dell'AST creato da astroid è possibile risalire al modulo di Sphinx.
def visit(node):
do(node)
for child in node.get_children():
if child:
visit(child)
def do(node):
if isinstance(node, astroid.nodes.ImportFrom):
ext = node.do_import_module(node.modname)
print(ext)
visit(tree)
Module.sphinx(name='sphinx', doc='The Sphinx documentation toolchain.', file='D:\\Coding\\Jupyter\\Py39\\ASTROID\\prj\\venv\\lib\\site-packages\\sphinx\\__init__.py', path=[ 'D:\\Coding\\Jupyter\\Py39\\ASTROID\\prj\\venv\\lib\\site-packages\\sphinx\\__init__.py'], package=True, pure_python=True, future_imports=set(), doc_node=<Const.str l.1 at 0x17224ff9ca0>, body=[ <Import l.6 at 0x17225288fd0>, <Import l.7 at 0x17225288fa0>, <Import l.8 at 0x17225288ee0>, <ImportFrom l.9 at 0x17225288e80>, <ImportFrom l.10 at 0x17225288d30>, <ImportFrom l.12 at 0x17225288c10>, <If l.16 at 0x17225288550>, <Expr l.19 at 0x17225288040>, <Expr l.21 at 0x172252880a0>, <Assign l.24 at 0x1722541af10>, <Assign l.25 at 0x1722541a730>, <Assign l.35 at 0x1722541a4c0>, <Assign l.37 at 0x1722541aa90>, <Assign l.39 at 0x1722541adc0>, <If l.40 at 0x17225050850>])
Non si ha però l'effettiva necessità di prapare preventivamente un altro ambiente virtuale vero e proprio, ma si possono semplicemente scaricare e installare le dipendenze in una cartella target nota usando pip e aggiungere tale cartella al path per la fase di parsing.
import subprocess
import sys
print(os.getcwd())
D:\Coding\Jupyter\Py39\ASTROID
REDOWNLOAD = True
INSTALL_FOLDER = "pip_installations"
if not os.path.exists(INSTALL_FOLDER) or REDOWNLOAD:
if os.path.exists(INSTALL_FOLDER):
shutil.rmtree(INSTALL_FOLDER)
os.mkdir(INSTALL_FOLDER)
subprocess.check_call([sys.executable, "-m", "pip", "download", "sphinx", "-d", INSTALL_FOLDER, "--no-deps", "--no-binary", ":all:"])
# Se va male solleva CalledProcessError
REDOWNLOAD = True
INSTALL_FOLDER = "pip_installations"
if not os.path.exists(INSTALL_FOLDER) or REDOWNLOAD:
if os.path.exists(INSTALL_FOLDER):
shutil.rmtree(INSTALL_FOLDER)
os.mkdir(INSTALL_FOLDER)
process = subprocess.Popen([sys.executable, "-m", "pip", "download", "asdadasfsdadsa", "-d", INSTALL_FOLDER, "--no-deps", "--no-binary", ":all:"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(process.communicate())
print(process.returncode)
# Se va male solleva CalledProcessError
(b'', b"ERROR: Could not find a version that satisfies the requirement asdadasfsdadsa (from versions: none)\r\nERROR: No matching distribution found for asdadasfsdadsa\r\nWARNING: You are using pip version 22.0.4; however, version 22.2.2 is available.\r\nYou should consider upgrading via the 'd:\\coding\\jupyter\\py39\\venv\\scripts\\python.exe -m pip install --upgrade pip' command.\r\n") 1
process = subprocess.Popen([sys.executable, "-m", "pip", "index", "versions", "sphinx"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = process.communicate()
msg = str(out).split("\\r\\n")[1]
assert msg.startswith("Available versions: ")
versions = msg.split(": ")[1].split(", ")
print(versions)
['5.2.3', '5.2.2', '5.2.1', '5.2.0.post0', '5.2.0', '5.1.1', '5.1.0', '5.0.2', '5.0.1', '5.0.0', '4.5.0', '4.4.0', '4.3.2', '4.3.1', '4.3.0', '4.2.0', '4.1.2', '4.1.1', '4.1.0', '4.0.3', '4.0.2', '4.0.1', '4.0.0', '3.5.4', '3.5.3', '3.5.2', '3.5.1', '3.5.0', '3.4.3', '3.4.2', '3.4.1', '3.4.0', '3.3.1', '3.3.0', '3.2.1', '3.2.0', '3.1.2', '3.1.1', '3.1.0', '3.0.4', '3.0.3', '3.0.2', '3.0.1', '3.0.0', '2.4.5', '2.4.4', '2.4.3', '2.4.2', '2.4.1', '2.4.0', '2.3.1', '2.3.0', '2.2.2', '2.2.1', '2.2.0', '2.1.2', '2.1.1', '2.1.0', '2.0.1', '2.0.0', '1.8.6', '1.8.5', '1.8.4', '1.8.3', '1.8.2', '1.8.1', '1.8.0', '1.7.9', '1.7.8', '1.7.7', '1.7.6', '1.7.5', '1.7.4', '1.7.3', '1.7.2', '1.7.1', '1.7.0', '1.6.7', '1.6.6', '1.6.5', '1.6.4', '1.6.3', '1.6.2', '1.6.1', '1.5.6', '1.5.5', '1.5.4', '1.5.3', '1.5.2', '1.5.1', '1.5', '1.4.9', '1.4.8', '1.4.7', '1.4.6', '1.4.5', '1.4.4', '1.4.3', '1.4.2', '1.4.1', '1.4', '1.3.6', '1.3.5', '1.3.4', '1.3.3', '1.3.2', '1.3.1', '1.3', '1.2.3', '1.2.2', '1.2.1', '1.2', '1.1.3', '1.1.2', '1.1.1', '1.1', '1.0.8', '1.0.7', '1.0.6', '1.0.5', '1.0.4', '1.0.3', '1.0.2', '1.0.1', '1.0', '0.6.7', '0.6.6', '0.6.5', '0.6.4', '0.6.3', '0.6.2', '0.6.1', '0.6', '0.5.2', '0.5.1', '0.5', '0.4.3', '0.4.2', '0.4.1', '0.4', '0.3', '0.2', '0.1.61950', '0.1.61945', '0.1.61843', '0.1.61798', '0.1.61611']
"""
assert len(os.listdir(INSTALL_FOLDER)) == 1
abs_archive_path = os.path.join(INSTALL_FOLDER, os.listdir(INSTALL_FOLDER)[0])
"""
'\nassert len(os.listdir(INSTALL_FOLDER)) == 1\nabs_archive_path = os.path.join(INSTALL_FOLDER, os.listdir(INSTALL_FOLDER)[0])\n'
"""
with tarfile.open(abs_archive_path) as f_archive:
f_archive.extractall(INSTALL_FOLDER)
os.remove(abs_archive_path)
"""
'\nwith tarfile.open(abs_archive_path) as f_archive:\n f_archive.extractall(INSTALL_FOLDER)\nos.remove(abs_archive_path)\n'
Ridimostriamo come prima che Sphinx non è disponibile, ma lo diventa aggiungendo tale cartella al path.
try:
import sphinx
except Exception as e:
print(e)
sys.path.insert(0, os.path.abspath(INSTALL_FOLDER))
astroid_brain_wash()
path_1 = "prj/outer_md.py"
name_1 = os.path.splitext(os.path.split(path_1)[1])[0]
with open(path_1, "r", encoding="utf8") as f1:
code_text_1 = f1.read()
t = '\n'.join([S+line for line in code_text_1.split('\n')]); print(f"Modulo 1:\n{t}")
tree = astroid.parse(code_text_1, path=path_1, module_name=name_1)
Modulo 1: from sphinx import * a: RemovedInNextVersionWarning() = None
sys.path = sys.path[1:]
sys.path
['D:\\Coding\\Jupyter\\Py39\\ASTROID', 'C:\\Program Files\\Python39\\python39.zip', 'C:\\Program Files\\Python39\\DLLs', 'C:\\Program Files\\Python39\\lib', 'C:\\Program Files\\Python39', 'd:\\coding\\jupyter\\py39\\venv', '', 'd:\\coding\\jupyter\\py39\\venv\\lib\\site-packages', 'd:\\coding\\jupyter\\py39\\venv\\lib\\site-packages\\win32', 'd:\\coding\\jupyter\\py39\\venv\\lib\\site-packages\\win32\\lib', 'd:\\coding\\jupyter\\py39\\venv\\lib\\site-packages\\Pythonwin']
try:
import sphinx
except Exception as e:
print(e)
def visit(node):
do(node)
for child in node.get_children():
if child:
visit(child)
def do(node):
if isinstance(node, astroid.nodes.ImportFrom):
ext = node.do_import_module(node.modname)
print(ext)
visit(tree)
Module.sphinx(name='sphinx', doc='The Sphinx documentation toolchain.', file='D:\\Coding\\Jupyter\\Py39\\ASTROID\\prj\\venv\\lib\\site-packages\\sphinx\\__init__.py', path=[ 'D:\\Coding\\Jupyter\\Py39\\ASTROID\\prj\\venv\\lib\\site-packages\\sphinx\\__init__.py'], package=True, pure_python=True, future_imports=set(), doc_node=<Const.str l.1 at 0x17224ff9ca0>, body=[ <Import l.6 at 0x17225288fd0>, <Import l.7 at 0x17225288fa0>, <Import l.8 at 0x17225288ee0>, <ImportFrom l.9 at 0x17225288e80>, <ImportFrom l.10 at 0x17225288d30>, <ImportFrom l.12 at 0x17225288c10>, <If l.16 at 0x17225288550>, <Expr l.19 at 0x17225288040>, <Expr l.21 at 0x172252880a0>, <Assign l.24 at 0x1722541af10>, <Assign l.25 at 0x1722541a730>, <Assign l.35 at 0x1722541a4c0>, <Assign l.37 at 0x1722541aa90>, <Assign l.39 at 0x1722541adc0>, <If l.40 at 0x17225050850>])
Ma cosa succede se una dipendenza del progetto è, per esempio, astroid, ed una sua versione potenzialmente vecchia e non adatta allo scopo di CodeOntology? Impostando preferenzialmente come path di ricerca quella in cui abbiamo scaricato le dipendenze del progetto, astroid durante il parsing cercherà prima lì per risolvere gli import, ma prenderà da lì anche le funzioni necessarie al parsing?
path_1 = "dependency_md.py"
path_2 = "dependency_dir/dependency_md.py"
with open(path_1, "r", encoding="utf8") as f1, \
open(path_2, "r", encoding="utf8") as f2:
code_text_1 = f1.read()
code_text_2 = f2.read()
t = '\n'.join([S+line for line in code_text_1.split('\n')]); print(f"Modulo 1:\n{t}")
t = '\n'.join([S+line for line in code_text_2.split('\n')]); print(f"Modulo 2:\n{t}")
Modulo 1: def f(): print(f"Modulo dipendenza per l'esecuzione") Modulo 2: def f(): print(f"Modulo dipendenza per il parsing")
Importiamo una versione di un modulo
import dependency_md
Aggiungiamo in cima al path di ricerca una cartella che contiene un modulo identico a quello già caricato
sys.path.insert(0, os.path.abspath("dependency_dir"))
Eseguiamo una funzione da tale modulo
dependency_md.f()
Modulo dipendenza per l'esecuzione
Ricarichiamo il modulo e rieseguiamo la stessa funzione.
importlib.reload(dependency_md);
dependency_md.f()
Modulo dipendenza per il parsing
Le funzioni usate sono quelle messe a disposizione dall'import nel momento in cui è eseguito. Modifiche successive al path non creano conflitti a meno che i moduli suddetti non siano ricaricati.
In dependency_dir è inoltre presente un modulo chiamato astroid, proprio come la libreria di parsing che utilizziamo. Possiamo parsare un modulo che richiama questo astroid fittizio usando l'astroid originale senza conflitti!
path_1 = "cite_astroid_md.py"
name_1 = os.path.splitext(os.path.split(path_1)[1])[0]
with open(path_1, "r", encoding="utf8") as f1:
code_text_1 = f1.read()
t = '\n'.join([S+line for line in code_text_1.split('\n')]); print(f"Modulo 1:\n{t}")
tree = astroid.parse(code_text_1, path=path_1, module_name=name_1)
Modulo 1: import astroid astroid.f()
def visit(node):
do(node)
for child in node.get_children():
if child:
visit(child)
def do(node):
if isinstance(node, astroid.nodes.Import):
mod_name = node.names[0][0]
ext = node.do_import_module(mod_name)
print(ext)
visit(tree)
Module.astroid(name='astroid', doc=None, file='D:\\Coding\\Jupyter\\Py39\\ASTROID\\dependency_dir\\astroid.py', path=[ 'D:\\Coding\\Jupyter\\Py39\\ASTROID\\dependency_dir\\astroid.py'], package=False, pure_python=True, future_imports=set(), doc_node=None, body=[<FunctionDef.f l.2 at 0x17224ff65b0>])
Puliamo infine il path.
sys.path = sys.path[1:]
Abbiamo visto come riferirsi a librerie con lo stesso nome per esempio di astroid, ma sorgente diverso. Noi stiamo usando inoltre Python3.9 o Python3.10: cosa succede se il nostro progetto utilizza, non so, Python3.3 o superiore? Le modifiche tra una versione e l'altra di Python3 sono minori, ma potrebbero esserci. L'idea è scaricare il sorgente della versione minima di Python con cui funziona tale libreria e metterlo in cima a sys.path come fatto sopra.
Bisogna però scaricare Python! Vedi qui le versioni.
import re as regex
url = "https://www.python.org/downloads/"
response = requests.get(url)
html = str(response.content)
__REGEX_PY_VERSION = r"[2-3](\.[0-9]+)*"
__REGEX_HTML_RELEASE = r'<a href="/downloads/release/python-[0-9]+/">Python (' + __REGEX_PY_VERSION + ')</a>'
for release_match in regex.finditer(__REGEX_HTML_RELEASE, html):
released_version = regex.search(__REGEX_PY_VERSION+'(?=<)', release_match.group(0)).group(0)
print(released_version)
3.7.14 3.8.14 3.9.14 3.10.7 3.10.6 3.10.5 3.9.13 3.10.4 3.9.12 3.10.3 3.9.11 3.8.13 3.7.13 3.9.10 3.10.2 3.10.1 3.9.9 3.9.8 3.10.0 3.7.12 3.6.15 3.9.7 3.8.12 3.9.6 3.8.11 3.7.11 3.6.14 3.9.5 3.8.10 3.9.4 3.8.9 3.9.2 3.8.8 3.6.13 3.7.10 3.8.7 3.9.1 3.9.0 3.8.6 3.5.10 3.7.9 3.6.12 3.8.5 3.8.4 3.7.8 3.6.11 3.8.3 2.7.18 3.7.7 3.8.2 3.8.1 3.7.6 3.6.10 3.5.9 3.5.8 2.7.17 3.7.5 3.8.0 3.7.4 3.6.9 3.7.3 3.4.10 3.5.7 2.7.16 3.7.2 3.6.8 3.7.1 3.6.7 3.5.6 3.4.9 3.7.0 3.6.6 2.7.15 3.6.5 3.4.8 3.5.5 3.6.4 3.6.3 3.3.7 2.7.14 3.4.7 3.5.4 3.6.2 3.6.1 3.4.6 3.5.3 3.6.0 2.7.13 3.4.5 3.5.2 2.7.12 3.4.4 3.5.1 2.7.11 3.5.0 2.7.10 3.4.3 2.7.9 3.4.2 3.3.6 3.2.6 2.7.8 2.7.7 3.4.1 3.4.0 3.3.5 3.3.4 3.3.3 2.7.6 2.6.9 3.3.2 3.2.5 2.7.5 3.2.4 3.3.1 2.7.4 3.3.0 2.6.8 3.2.3 3.1.5 2.7.3 3.2.2 3.2.1 2.7.2 3.1.4 2.6.7 2.5.6 3.2.0 2.7.1 3.1.3 2.6.6 2.7.0 3.1.2 2.6.5 2.5.5 2.6.4 2.6.3 3.1.1 3.1.0 2.6.2 3.0.1 2.5.4 2.5.3 2.4.6 2.6.1 3.0.0 2.6.0 2.3.7 2.4.5 2.5.2 2.5.1 2.3.6 2.4.4 2.5.0 2.4.3 2.4.2 2.4.1 2.3.5 2.4.0 2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.3 2.2.2 2.2.1 2.1.3 2.2.0 2.0.1
py_vers = "3.3.1"
download_url = f"https://www.python.org/ftp/python/{py_vers}/Python-{py_vers}.tgz"
compressed_file_name = f"Python-{py_vers}.tgz"
uncompress_dir = "dependency_dir"
response = requests.get(download_url)
if response.status_code == 200:
print(f"Versione disponibile.")
with open(compressed_file_name, "wb") as f:
f.write(response.content)
with tarfile.open(compressed_file_name) as f:
f.extractall(uncompress_dir)
else:
print(f"Versione non disponibile.")
Versione disponibile.
with tarfile.open("pip_installations/Sphinx-5.1.1.tar.gz") as f:
print(f.list())
# f.extractall("pip_installations")
# os.remove("pip_installations/Sphinx-5.1.1.tar.gz")
--------------------------------------------------------------------------- FileNotFoundError Traceback (most recent call last) Input In [85], in <cell line: 1>() ----> 1 with tarfile.open("pip_installations/Sphinx-5.1.1.tar.gz") as f: 2 print(f.list()) File C:\Program Files\Python39\lib\tarfile.py:1611, in TarFile.open(cls, name, mode, fileobj, bufsize, **kwargs) 1609 saved_pos = fileobj.tell() 1610 try: -> 1611 return func(name, "r", fileobj, **kwargs) 1612 except (ReadError, CompressionError): 1613 if fileobj is not None: File C:\Program Files\Python39\lib\tarfile.py:1675, in TarFile.gzopen(cls, name, mode, fileobj, compresslevel, **kwargs) 1672 raise CompressionError("gzip module is not available") 1674 try: -> 1675 fileobj = GzipFile(name, mode + "b", compresslevel, fileobj) 1676 except OSError: 1677 if fileobj is not None and mode == 'r': File C:\Program Files\Python39\lib\gzip.py:173, in GzipFile.__init__(self, filename, mode, compresslevel, fileobj, mtime) 171 mode += 'b' 172 if fileobj is None: --> 173 fileobj = self.myfileobj = builtins.open(filename, mode or 'rb') 174 if filename is None: 175 filename = getattr(fileobj, 'name', '') FileNotFoundError: [Errno 2] No such file or directory: 'pip_installations/Sphinx-5.1.1.tar.gz'
shutil.move(os.path.abspath("pip_installations/Sphinx-5.1.1"), os.path.abspath("."))
shutil.rmtree("pip_installations")
https://files.pythonhosted.org/packages/3a/30/ac07935542607c876f3fcee1c1ab043d253332567009994a1bf71d9b55cd/Sphinx-5.1.1.tar.gz
# SEE https://docs.python.org/3/library/argparse.html
path_1 = "params_md_1.py"
name_1 = os.path.splitext(os.path.split(path_1)[1])[0]
path_2 = "params_pkg/params_md_2.py"
path_3 = "params_pkg/params_nested_pkg/params_md_3.py"
with open("params_md_1.py", "r", encoding="utf8") as f1, \
open("params_pkg/params_md_2.py", "r", encoding="utf8") as f2, \
open("params_pkg/params_nested_pkg/params_md_3.py", "r", encoding="utf8") as f3:
code_text_1 = f1.read()
code_text_2 = f2.read()
code_text_3 = f3.read()
t = '\n'.join([S+line for line in code_text_1.split('\n')]); print(f"Modulo 1:\n{t}")
t = '\n'.join([S+line for line in code_text_2.split('\n')]); print(f"Modulo 2:\n{t}")
t = '\n'.join([S+line for line in code_text_3.split('\n')]); print(f"Modulo 3:\n{t}")
tree = astroid.parse(code_text_1, path=path_1, module_name=name_1)
def visit(node):
inferencing(node)
for child in node.get_children():
if child:
visit(child)
def inferencing(node):
try:
print(f"Node of type {type(node).__name__}")
node_str = "\n".join([S+line for line in node.__str__().split("\n")])
print(node_str)
print(f"Inferred results:")
i = 1
for infer_val in node.infer():
infer_val_str = "\n".join([S+line for line in infer_val.__str__().split("\n")])
print(f"{S}{i:02}\n{infer_val_str}")
i += 1
except Exception as e:
print(f"Something went wrong! {e} ({type(e).__name__})")
finally:
print("\n")
visit(tree)
path_1 = "class_name.py"
name_1 = os.path.splitext(os.path.split(path_1)[1])[0]
with open(path_1, "r", encoding="utf8") as f1:
code_text_1 = f1.read()
t = '\n'.join([S+line for line in code_text_1.split('\n')]); print(f"Modulo 1:\n{t}")
tree = astroid.parse(code_text_1, path=path_1, module_name=name_1)
def visit(node):
do(node)
for child in node.get_children():
if child:
visit(child)
def do(node):
if isinstance(node, astroid.nodes.ClassDef):
scope_hierarchy_names = []
scope = node.scope()
while not isinstance(scope, astroid.nodes.Module):
scope_hierarchy_names.insert(0, scope.name)
scope = scope.parent.scope()
# print(f"{'.'.join(scope_hierarchy_names)}")
# print(node.doc_node.value)
if isinstance(node, astroid.nodes.Arguments):
for arg in node.args:
print(arg.name)
visit(tree)