Created
July 24, 2023 16:17
-
-
Save jymchng/cf5eccb783688f383dcbc6fe6ed214c1 to your computer and use it in GitHub Desktop.
Python consts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ast | |
from ast import NodeVisitor | |
import inspect | |
import textwrap | |
from types import MethodType | |
from typing_extensions import Annotated | |
from abc import ABCMeta | |
from pydantic.main import ModelMetaclass | |
class AssignVisitor(NodeVisitor): | |
class NameVisitorToFindConst(NodeVisitor): | |
def __init__(self): | |
self.found = False | |
def visit_Name(self, node): | |
if node.id == 'const': | |
self.found = True | |
def __init__(self, consts_dict: dict, strict=False): | |
self.consts_dict = consts_dict | |
self.strict = strict | |
self.exceptions_strs = [] | |
def visit_Assign(self, node): | |
targets = node.targets | |
for target in targets: | |
if not isinstance(target, ast.Attribute): | |
return | |
var_name = target.attr | |
if self.strict: | |
self.exceptions_strs.append( | |
f'> Cannot re-assign any instance or class variable `{var_name}` in a method decorated with `const`') | |
return | |
if var_name in self.consts_dict: | |
self.exceptions_strs.append( | |
f'> Cannot re-assign `{var_name}` as it is a `const` variable') | |
def visit_AnnAssign(self, node): | |
if not isinstance(node.target, ast.Attribute): | |
return | |
var_name = node.target.attr | |
if self.strict: | |
self.exceptions_strs.append( | |
f'> Cannot re-assign any instance or class variable `{var_name}` in a method decorated with `const`') | |
return | |
if var_name in self.consts_dict: | |
if 'const' not in node.annotation.id: | |
self.exceptions_strs.append( | |
f'> Cannot change the annotation of a `const` variable') | |
return | |
self.exceptions_strs.append( | |
f'> Cannot re-assign `{var_name}` as it is a `const` variable') | |
return | |
if node.value is None: | |
self.exceptions_strs.append( | |
f'> `const` variable `{var_name}` must be declared with a value') | |
return | |
# if not isinstance(node.value, ast.Constant): | |
# self.exceptions_strs.append( | |
# f'> `const` variable `{var_name}` when declared, must be declared with a `Constant` value, the value declared is dynamic variable `{node.value.id}`') | |
# return | |
# else: | |
# if not hasattr(node.value, 'value'): | |
# self.exceptions_strs.append( | |
# f'> `const` variable `{var_name}` when declared, it must be declared with a `Constant` value, the value declared is dynamic variable `{node.value.id}`') | |
# return | |
name_node_visitor = self.NameVisitorToFindConst() | |
name_node_visitor.visit(node.annotation) | |
if name_node_visitor.found: | |
self.consts_dict[var_name] = const | |
def visit_AugAssign(self, node): | |
var_name = node.target.attr | |
if not isinstance(node.target, ast.Attribute): | |
return | |
if self.strict: | |
self.exceptions_strs.append( | |
f'> Cannot re-assign any instance or class variable `{var_name}` in a method decorated with `const`') | |
return | |
if var_name in self.consts_dict: | |
self.exceptions_strs.append( | |
f'> Cannot re-assign `{var_name}` as it is a `const` variable') | |
return | |
class const(str): | |
def __new__(self, func=None): | |
if func is not None: | |
func.__const__ = True | |
return func | |
return str.__new__(self, "const") | |
def __class_getitem__(self, index): | |
if not isinstance(index, (tuple,)): | |
index = index, | |
index = tuple([_ for _ in index]) | |
return Annotated[const, index] | |
MappingProxy = type(const.__dict__) | |
def __setattr__(slf, _name, _value): | |
if _name in slf._consts_ and not slf._init: | |
raise Exception( | |
f'Cannot re-assign `{_name}` since it is annotated as `const`') | |
super(slf.__class__, slf).__setattr__(_name, _value) | |
def in_pydantic_fields(attrs, k): | |
return attrs['__fields__'].get(k, False) | |
def is_pydantic(attrs): | |
return '__fields__' in attrs | |
class ConstsMeta(ABCMeta): | |
def __new__(mcls, cls, bases=(), attrs={}): | |
bases = cls.__bases__ | |
attrs = dict(cls.__dict__) | |
_consts_ = {} | |
exceptions_strs = [] | |
print(mcls, cls, bases, attrs) | |
if hasattr(cls, '__annotations__'): | |
annotations = cls.__annotations__ | |
else: | |
annotations = {} | |
for k, v in annotations.items(): | |
if (type(v) is const) or ('const' in str(v)) or v is const: | |
if not hasattr(cls, k): | |
if is_pydantic(attrs): | |
if not in_pydantic_fields(attrs, k): | |
exceptions_strs.append( | |
f"> Pydantic field {k} is labelled as `const`, but it is not defined with a value") | |
else: | |
_consts_[k] = k | |
else: | |
exceptions_strs.append( | |
f">>> Class variable {k} is labelled as `const`, but it is not defined with a value") | |
continue | |
else: | |
_consts_[k] = attrs.pop(k) | |
elif hasattr(v, '__origin__'): | |
origin = v.__origin__ | |
if hasattr(v, '__extra__'): | |
extra = v.__extra__ | |
else: | |
extra = v.__metadata__ | |
if (origin is const) or (origin=='const') or (const in extra) or ('const' in extra): | |
if not hasattr(cls, k): | |
exceptions_strs.append( | |
f">> Class variable {k} is labelled as `const`, but it is not defined with a value") | |
elif not in_pydantic_fields(attrs, k): | |
exceptions_strs.append( | |
f"> Pydantic field {k} is labelled as `const`, but it is not defined with a value") | |
else: | |
_consts_[k] = attrs.pop(k) | |
else: | |
pass | |
for k, v in attrs.items(): | |
if callable(v) or isinstance(v, classmethod): | |
if isinstance(v, classmethod): | |
v = v.__func__ | |
if isinstance(v, staticmethod): | |
continue | |
if hasattr(v, '__const__'): | |
assign_visitor_strict = AssignVisitor(_consts_, True) | |
tree = ast.parse( | |
textwrap.dedent( | |
inspect.getsource( | |
v.__code__))) | |
assign_visitor_strict.visit(tree) | |
if assign_visitor_strict.exceptions_strs: | |
indivi_exceptions = "\n\t".join( | |
assign_visitor_strict.exceptions_strs) | |
exceptions_strs.append( | |
f'> Method of type `{type(v)}` with name `{v.__name__}` raised an (or many) exception(s):\n\t{indivi_exceptions}') | |
_consts_.update(assign_visitor_strict.consts_dict) | |
else: | |
if not isinstance(v, type): # don't check for inner classes | |
assign_visitor = AssignVisitor(_consts_) | |
tree = ast.parse( | |
textwrap.dedent( | |
inspect.getsource( | |
v.__code__))) | |
assign_visitor.visit(tree) | |
if assign_visitor.exceptions_strs: | |
indivi_exceptions = "\n\t".join( | |
assign_visitor.exceptions_strs) | |
exceptions_strs.append( | |
f'> Method of type `{type(v)}` with name `{v.__name__}` raised an (or many) exception(s):\n\t{indivi_exceptions}') | |
_consts_.update(assign_visitor.consts_dict) | |
if exceptions_strs: | |
exceptions_strs[0] = f'\nFor class `{cls.__name__}`:\n' + \ | |
exceptions_strs[0] | |
raise Exception("\n".join(exceptions_strs)) | |
_consts_ = MappingProxy(_consts_) | |
attrs.update({'_consts_': _consts_}) | |
return type.__new__(mcls, cls.__name__, bases, dict(attrs)) | |
def __call__(cls, *args, **kwargs): | |
cls._init = True | |
inst = cls.__new__(cls, *args, **kwargs) | |
setattr(inst.__class__, '__setattr__', MethodType(__setattr__, inst)) | |
inst.__init__(*args, **kwargs) | |
cls._init = False | |
return inst | |
def __setattr__(cls, __name, __value): | |
if __name in cls._consts_: | |
raise Exception( | |
f'Cannot re-assign `{__name}` since it is annotated as `const`') | |
super().__setattr__(__name, __value) | |
class ConstsPydanticMeta(ConstsMeta, ModelMetaclass): | |
... | |
consts = ConstsPydanticMeta | |
try: | |
@consts | |
class A: | |
# `B` declared as `const`, none of class `A`'s method can mutate it | |
B: const[str] = 'B' | |
# `K` declared as `const` but no value is assigned to it, raises Exception: | |
# > Class variable K is labelled as `const`, but it is not defined with a value | |
K: const[tuple] | |
# OK, `G` declared as `const`, none of class `A`'s method can mutate it | |
G: const = 52 | |
# Not OK | |
# > Class variable KK is labelled as `const`, but it is not defined with a value | |
KK: const | |
# `H` and `HH` are declared as `const`, none of class `A`'s method can mutate it | |
# note the annotation can be very flexible | |
H: Annotated[int, const] = 22 | |
HH: Annotated[const, int, str] = 55 | |
HHH: Annotated[int, 'const', str] = 22 | |
# annotated with str also works | |
HHHH: 'const[int]' = 77 | |
D = 'D' | |
def __init__(self, E=57): | |
# `E` is an instance variable, assigned with a value dynamically and annotated as `const` | |
# all subsequent method calls post-init cannot mutate it, detected at reading of class definition | |
# and enforced subsequently | |
self.E: const[int] = E | |
# OK, normal instance variable | |
self.F: str = E | |
# declared `GGG` and `GGGG` as `const` during initialization | |
self.GGG: Annotated[const, str] = E | |
self.GGGG: Annotated[str, const, int] = E | |
# error > Cannot re-assign everything below as they are `const` variables | |
# these exceptions are raised when reading the class definition | |
self.HH = 33 | |
self.HHH = 23 | |
self.HHHH = 23 | |
self.H = 33 | |
self.G = 50 | |
def hello(self): | |
self.F = 33 | |
print("Hello") | |
# all methods are checked for illegal re-assignment of values to `const` variables | |
def changing_C(self): | |
self.C = 'D' | |
self.E = 55 | |
self.GGG = 56 | |
self.GGGG = 56 | |
# method decorated with `const` cannot have any instance attribute being reassigned to a different value | |
# this is check when reading the class definition, not enforced subsequently because | |
# exception is raised | |
@const | |
def promise_not_to_change(self): | |
self.D = 'F' # should raise Exception because whole method is `const` | |
self.E = 'ZS' | |
@staticmethod | |
def bye(): | |
print("Bye") | |
@classmethod | |
def hey(cls): | |
cls.B = 'BB' | |
print("Hey") | |
except Exception as err: | |
# these exceptions are raised when reading the class definition | |
# class `A` has not been instantiated | |
print(err) | |
class G: | |
... | |
@consts | |
class B(G, str): | |
B: const[str] = 'B' | |
C: const = 'C' | |
D = 'D' | |
def __new__(cls, E): | |
inst = str.__new__(cls, "HELLO!!!") | |
return inst | |
def __init__(self, E): | |
self.E: const = E | |
self.EE: const[int] = 55 | |
self.EEE: Annotated[const, int] = 66 | |
self.EEEE: Annotated[int, const, str] = 27 | |
self.F = 'F' | |
@const | |
def hello(self): | |
print("Hello") | |
@staticmethod | |
def bye(): | |
print("Bye") | |
@classmethod | |
def hey(cls): | |
print("Hey") | |
b = B(E=55) | |
print(str.__str__(b)) | |
b.hello() | |
try: | |
b.B = 'hi' | |
except Exception as err: | |
print(err) | |
try: | |
b.C = 'hi' | |
except Exception as err: | |
print(err) | |
try: | |
b.E = 'hi' | |
except Exception as err: | |
print(err) | |
try: | |
b.EE = 'hi' | |
except Exception as err: | |
print(err) | |
try: | |
b.EEE = 'hi' | |
except Exception as err: | |
print(err) | |
try: | |
b.EEEE = 'hi' | |
except Exception as err: | |
print(err) | |
try: | |
B.B = 'hi' | |
except Exception as err: | |
print(err) | |
try: | |
B.C = 'hi' | |
except Exception as err: | |
print(err) | |
try: | |
B.E = 'hi' | |
except Exception as err: | |
print(err) | |
try: | |
B.EE = 'hi' | |
except Exception as err: | |
print(err) | |
try: | |
B.EEE = 'hi' | |
except Exception as err: | |
print(err) | |
try: | |
B.EEEE = 'hi' | |
except Exception as err: | |
print(err) | |
# B.__dict__['E'] = 68 | |
# b.__dict__['E'] = 68 | |
import pydantic | |
try: | |
@consts | |
class Model(pydantic.BaseModel): | |
first: str | |
last: const[str] = pydantic.Field(default='Boolean') | |
role: const[str] = pydantic.Field(default='Admin') | |
def change_last(self, new_last): | |
self.last = new_last | |
except Exception as err: | |
print(err) | |
@consts | |
class Model(pydantic.BaseModel): | |
first: str | |
last: const[str] = pydantic.Field(default='Boolean') | |
role: const[str] = pydantic.Field(default='Admin') | |
try: | |
m = Model(first='james') | |
m.last = 'hi' | |
print(m.last) | |
except Exception as err: | |
print(err) | |
try: | |
Model.last = 'hi' | |
print(Model.last) | |
except Exception as err: | |
print(err) |
Author
jymchng
commented
Nov 14, 2023
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment