Skip to content

Commit

Permalink
Version 3.2.1 - Stubber changes and fixes (#4)
Browse files Browse the repository at this point in the history
### Changed

- Stubber now has an option to only "export" the resulting "Final" stub
  class and keeping all the other stubs "private" and this option is True by
  default (they don't really need to be public)
- You can now give the resulting "Final" config stub a custom name
- You can also give the "Final" config stub an empty string for a name which
  will simply omit generating it (for custom/complex composition of stubs
  later on if people want)
- All stubs now also inherit from `dict` because any attribute in
  `BaseConfig` that's a Map (and thus has a stub class) will also behave as
  a `dict` (even if `Empty`)

### Fixed

- Stubber now appends an underscore to stubs with Python reserved keywords
  like `class` and `def` etc.
- The `BaseConfig` object now also spots attribute fetching of those
  keywords with an appended underscore and fetches the correct attribute
  nevertheless.
  • Loading branch information
CCP-Zeulix authored Apr 30, 2024
1 parent 1f40c0a commit 6e640d6
Show file tree
Hide file tree
Showing 22 changed files with 474 additions and 207 deletions.
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [3.2.1] - 2024-04-29

### Changed

- Stubber now has an option to only "export" the resulting "Final" stub
class and keeping all the other stubs "private" and this option is True by
default (they don't really need to be public)
- You can now give the resulting "Final" config stub a custom name
- You can also give the "Final" config stub an empty string for a name which
will simply omit generating it (for custom/complex composition of stubs
later on if people want)
- All stubs now also inherit from `dict` because any attribute in
`BaseConfig` that's a Map (and thus has a stub class) will also behave as
a `dict` (even if `Empty`)

### Fixed

- Stubber now appends an underscore to stubs with Python reserved keywords
like `class` and `def` etc.
- The `BaseConfig` object now also spots attribute fetching of those
keywords with an appended underscore and fetches the correct attribute
nevertheless.


## [3.2.0] - 2024-04-22

### Added
Expand Down
2 changes: 1 addition & 1 deletion alviss/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = '3.2.0'
__version__ = '3.2.1'

__author__ = 'Thordur Matthiasson <thordurm@ccpgames.com>'
__license__ = 'MIT License'
Expand Down
13 changes: 11 additions & 2 deletions alviss/cli/stubber/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ def main():
parser.add_argument('-o', '--output', help='File to write the generated stub code to (otherwise its just printed to stdout)',
default='', nargs='?')
parser.add_argument('-f', '--force-overwrite', help='Overwrite existing output file if it exists', action='store_true')
parser.add_argument('-n', '--class-name',
help='The name of the resulting "final" stub class generated when outputting to a file (default is "AlvissConfigStub"). Set to "None" to skip generating the "final" class.',
default='AlvissConfigStub')
parser.add_argument('-x', '--export-all', help='Make all stub class names public and export via __all__ when outputting to a file.',
action='store_true')

loudness_group = parser.add_mutually_exclusive_group()
loudness_group.add_argument('-s', '--silent', action='store_true',
Expand All @@ -37,12 +42,16 @@ def main():

stubber.SimpleStubMaker().render_stub_classes_to_file(input_file=args.file,
output_file=args.output,
overwrite_existing=args.force_overwrite)
overwrite_existing=args.force_overwrite,
is_private=not args.export_all)
else:
if not args.silent:
print(f'Printing results:')
print(f'==================================================')
print(stubber.SimpleStubMaker().render_stub_classes_from_descriptor_file(args.file))
cls_name = 'AlvissConfigStub' if args.class_name is None else args.class_name
if cls_name.lower().strip() == 'none':
cls_name = ''
print(stubber.SimpleStubMaker().render_stub_classes_from_descriptor_file(args.file, class_name=cls_name, is_private=not args.export_all))

if not args.silent:
print(f'==================================================')
Expand Down
42 changes: 36 additions & 6 deletions alviss/structs/baseconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,36 @@
from ccptools.tpu import string
import json
import yaml
import collections
from alviss.utils import *


class _KwSafeEmptyDict(EmptyDict):
def __getattribute__(self, name):
name = unescape_keyword(name)
if hasattr(collections.defaultdict, name):
return collections.defaultdict.__getattribute__(self, name)
if collections.defaultdict.__contains__(self, name):
data = collections.defaultdict.__getitem__(self, name)
if isinstance(data, dict):
return _KwSafeEmptyDict(**data)
elif data is None:
return Empty
else:
return data
return Empty

def __getitem__(self, item):
item = unescape_keyword(item)
if collections.defaultdict.__contains__(self, item):
data = collections.defaultdict.__getitem__(self, item)
if isinstance(data, dict):
return _KwSafeEmptyDict(**data)
elif data is None:
return Empty
else:
return data
return Empty


class BaseConfig(object):
Expand Down Expand Up @@ -41,21 +71,21 @@ class BaseConfig(object):
_secret_keys = {'pass', 'secret', 'token', 'key'}

def __init__(self, **kwargs):
super().__setattr__('_data', EmptyDict(**kwargs))
super().__setattr__('_data', _KwSafeEmptyDict(**kwargs))

def __getattr__(self, item):
return EmptyDict.__getattribute__(self._data, item)
return _KwSafeEmptyDict.__getattribute__(self._data, item)

def __setattr__(self, key, value):
if key.startswith('_'):
super().__setattr__(key, value)
self.update(**{key: value})

def __str__(self):
return EmptyDict.__str__(self._repr_dump(self._data)) # noqa
return _KwSafeEmptyDict.__str__(self._repr_dump(self._data)) # noqa

def __repr__(self):
return EmptyDict.__repr__(self._repr_dump(self._data)) # noqa
return _KwSafeEmptyDict.__repr__(self._repr_dump(self._data)) # noqa

def as_json(self, unmaksed: bool = False) -> str:
return json.dumps(self.as_dict(unmaksed=unmaksed), indent=4)
Expand Down Expand Up @@ -87,8 +117,8 @@ def _is_key_secret(cls, key: str):
return False

def load(self, **kwargs):
super().__setattr__('_data', EmptyDict(**kwargs))
super().__setattr__('_data', _KwSafeEmptyDict(**kwargs))

def update(self, **kwargs):
iters.nested_dict_update(self._data, EmptyDict(**kwargs))
iters.nested_dict_update(self._data, _KwSafeEmptyDict(**kwargs))

42 changes: 27 additions & 15 deletions alviss/stubber/stubmaker/_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,49 +14,61 @@


class SimpleStubMaker(IStubMaker):
def render_stub_classes_from_descriptor_file(self, file) -> str:
def render_stub_classes_from_descriptor_file(self, file, is_private: bool = True,
class_name: str = 'AlvissConfigStub') -> str:
cfg = autoload(file)
root_stub = StubClass.from_dict(cfg.as_dict(unmaksed=True))
root_stub = StubClass.from_dict(cfg.as_dict(unmaksed=True), is_private=is_private)
res = []
class_names = []
for stub in root_stub.get_all_sub_stubs():
class_names.append(stub.class_name)
if not stub.class_name.startswith('_'):
class_names.append(stub.class_name)
res.append(stub.render_class_str())
class_names.append(root_stub.class_name)
if not root_stub.class_name.startswith('_'):
class_names.append(root_stub.class_name)
res.append(root_stub.render_class_str())
class_str = '\n\n\n'.join(res)

all_str = '\n'.join([f" '{c}'," for c in class_names])
if class_name:
class_names.append(class_name)

return f"""__all__ = [
all_str = ''
if class_names:
all_str = '\n'.join([f" '{c}'," for c in class_names])
all_str = f"""__all__ = [
{all_str}
'AlvissConfigStub',
]
from typing import *
"""
root_cls = ''
if class_name:
root_cls = f"""
class {class_name}(BaseConfig, {root_stub.class_name}):
pass"""

return f"""{all_str}from typing import *
from alviss.structs import Empty
from alviss.structs.cfgstub import _BaseCfgStub
from alviss.structs import BaseConfig
{class_str}
class AlvissConfigStub(BaseConfig, CfgStub):
pass"""
{class_str}{root_cls}"""

def render_stub_classes_to_file(self, input_file: str, output_file: str, overwrite_existing: bool = False):
def render_stub_classes_to_file(self, input_file: str, output_file: str, overwrite_existing: bool = False, is_private: bool = True):
out = pathlib.Path(output_file).absolute()
if out.exists() and not overwrite_existing:
raise AlvissFileAlreadyExistsError('Output file already exists', file_name=output_file)

results = self.render_stub_classes_from_descriptor_file(input_file)
results = self.render_stub_classes_from_descriptor_file(input_file, is_private=is_private)

if not out.parent.exists():
log.debug(f'Creating output path: {out.parent}')
os.makedirs(out.parent, exist_ok=True)

with open(output_file, 'w') as fin:
fin.write(results)
fin.write('\n')

return
48 changes: 30 additions & 18 deletions alviss/stubber/stubmaker/_structs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import dataclasses
from typing import *
from alviss.structs.errors import *
from alviss.utils import *
import re

import logging
Expand Down Expand Up @@ -89,6 +90,10 @@ def _fix_and_validate_type_names(self):
else:
self.value = self._loop_over_type_words(self.value)

@property
def safe_field_name(self) -> str:
return escape_keyword(self.name)

@property
def is_required(self) -> bool:
return self.is_self_required or self.has_required_children
Expand All @@ -102,7 +107,8 @@ def has_required_children(self) -> bool:
@classmethod
def from_keyval(cls, key: str, value: Union[str, List, Dict], ancestors: Optional[List[str]],
is_map_of_stuff: bool = False,
pre_required: bool = False) -> 'StubField':
pre_required: bool = False,
is_private: bool = True) -> 'StubField':
required = pre_required

if key.endswith('*'):
Expand All @@ -115,10 +121,10 @@ def from_keyval(cls, key: str, value: Union[str, List, Dict], ancestors: Optiona
if sub_key.endswith('*'):
required = True
return cls.from_keyval(key=key, value=sub_val, pre_required=required,
ancestors=ancestors, is_map_of_stuff=True)
ancestors=ancestors, is_map_of_stuff=True, is_private=is_private)

else:
value = StubClass.from_dict(input_dict_or_list=value, field_name=key, ancestors=ancestors)
value = StubClass.from_dict(input_dict_or_list=value, field_name=key, ancestors=ancestors, is_private=is_private)
return cls(name=key, value=value, is_self_required=required,
ancestors=ancestors, is_map_of_stuff=is_map_of_stuff)

Expand All @@ -128,7 +134,7 @@ def from_keyval(cls, key: str, value: Union[str, List, Dict], ancestors: Optiona
field_name='.'.join(ancestors+[key]))

if isinstance(value[0], dict):
value = StubClass.from_dict(input_dict_or_list=value[0], field_name=key, ancestors=ancestors)
value = StubClass.from_dict(input_dict_or_list=value[0], field_name=key, ancestors=ancestors, is_private=is_private)
return cls(name=key, value=value, is_self_required=required, ancestors=ancestors,
is_dict_list=True, is_map_of_stuff=is_map_of_stuff)

Expand Down Expand Up @@ -163,11 +169,11 @@ def render_field_str(self) -> str:
if isinstance(self.value, StubClass):
if self.is_required:
if self.is_dict_list:
return f' {self.name}: List[{self.value.class_name}]'
return f' {self.safe_field_name}: List[{self.value.class_name}]'
elif self.is_map_of_stuff:
return f' {self.name}: Dict[str, {self.value.class_name}]'
return f' {self.safe_field_name}: Dict[str, {self.value.class_name}]'
else:
return f' {self.name}: {self.value.class_name}'
return f' {self.safe_field_name}: {self.value.class_name}'
else:
if self.is_dict_list:
type_list = [f'List[{self.value.class_name}]', 'Empty']
Expand All @@ -182,18 +188,19 @@ def render_field_str(self) -> str:
type_list.append('Empty')
else:
if self.is_required:
return f' {self.name}: {self.value}'
return f' {self.safe_field_name}: {self.value}'
else:
type_list = [self.value, 'Empty']

return f' {self.name}: Union[{", ".join(type_list)}]'
return f' {self.safe_field_name}: Union[{", ".join(type_list)}]'


@dataclasses.dataclass
class StubClass:
name: str
ancestors: List[str] = dataclasses.field(default_factory=list)
fields: List[StubField] = dataclasses.field(default_factory=list)
is_private: bool = True

@property
def has_required_fields(self) -> bool:
Expand All @@ -206,18 +213,19 @@ def has_required_fields(self) -> bool:
def from_dict(cls,
input_dict_or_list: Union[Dict[str, Any], List[Any]],
field_name: str = '',
ancestors: Optional[List[str]] = None) -> 'StubClass':
ancestors: Optional[List[str]] = None,
is_private: bool = True) -> 'StubClass':
ancestors = ancestors or []
ancestors.append(field_name)
if isinstance(input_dict_or_list, dict):
fields = [StubField.from_keyval(k, v, ancestors.copy()) for k, v in input_dict_or_list.items()]
fields = [StubField.from_keyval(k, v, ancestors.copy(), is_private=is_private) for k, v in input_dict_or_list.items()]
elif isinstance(input_dict_or_list, list): # Should you ever get a list...?!?
log.warning('NOT SURE THIS SHOULD EVER HAPPEN!!!')
fields = [StubField.from_keyval('__list__', input_dict_or_list[0], ancestors.copy())]
fields = [StubField.from_keyval('__list__', input_dict_or_list[0], ancestors.copy(), is_private=is_private)]
else:
raise AlvissStubberSyntaxError(f'Unexpected type fed to StubClass.from_dict: {type(input_dict_or_list)}')
ancestors.pop()
return cls(name=field_name, ancestors=ancestors, fields=fields)
return cls(name=field_name, ancestors=ancestors, fields=fields, is_private=is_private)

@staticmethod
def field_name_to_class_name(field_name: str) -> str:
Expand All @@ -239,20 +247,24 @@ def field_name_to_class_name(field_name: str) -> str:
if not part:
continue
buff.append(part.capitalize())
return ''.join(buff)
result = ''.join(buff)
return escape_keyword(result) # Just in case!

@property
def class_name(self) -> str:
start = 'Cfg'
if self.is_private:
start = '_Cfg'
if not self.name:
return 'CfgStub'
return f'{start}Stub'
else:
if self.ancestors:
return f'Cfg{"".join([self.field_name_to_class_name(a) for a in self.ancestors+[self.name]])}Stub'
return f'{start}{"".join([self.field_name_to_class_name(a) for a in self.ancestors+[self.name]])}Stub'
else:
return f'Cfg{self.field_name_to_class_name(self.name)}Stub'
return f'{start}{self.field_name_to_class_name(self.name)}Stub'

def render_class_str(self) -> str:
lines = [f'class {self.class_name}(_BaseCfgStub):']
lines = [f'class {self.class_name}(_BaseCfgStub, dict):']
for field in self.fields:
lines.append(field.render_field_str())
return '\n'.join(lines)
Expand Down
6 changes: 4 additions & 2 deletions alviss/stubber/stubmaker/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@

class IStubMaker(abc.ABC):
@abc.abstractmethod
def render_stub_classes_from_descriptor_file(self, file: str) -> str:
def render_stub_classes_from_descriptor_file(self, file: str, is_private: bool = True,
class_name: str = 'AlvissConfigStub') -> str:
"""Renders a Python module file with type hinting stub classes from the
Alviss config type descriptor file.
"""
pass

@abc.abstractmethod
def render_stub_classes_to_file(self, input_file: str, output_file: str, overwrite_existing: bool = False):
def render_stub_classes_to_file(self, input_file: str, output_file: str,
overwrite_existing: bool = False, is_private: bool = True):
"""Writer the results of the `render_stub_classes_from_descriptor_file`
call to the given output file.
"""
Expand Down
1 change: 1 addition & 0 deletions alviss/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .kwfields import *
Loading

0 comments on commit 6e640d6

Please sign in to comment.