Skip to main content

Metaclass utilities for Python

Project description

# Py Meta Utils

## The Meta Options Factory Pattern as a library, and related metaclass utilities

OK, but just what is the Meta options factory pattern? Perhaps the easiest way to explain it is to start with an example. Let's say you wanted your end users to be able to optionally enable logging of the actions of a class from a library you're writing:

```python
class EndUserClass(YourLoggableService):
class Meta:
debug: bool = True
verbosity: int = 2
log_destination: str = '/tmp/end-user-class.log'
```

The first step is to define your custom `MetaOption` subclasses:

- All that's absolutely required to implement is the constructor and its `name` argument. That said, it's recommended to also specify the `default` and `inherit` arguments for the sake of being explicit.
- The `check_value` method is optional, but useful for making sure your users aren't giving you garbage.
- The `get_value` method has a default implementation that normally you shouldn't need to override, unless your default value is mutable or you have advanced logic.
- There's also a `contribute_to_class` method that we'll cover later on.

```python
import os
import sys

# first we have to import what we need from py_meta_utils
from py_meta_utils import (McsArgs, MetaOption, MetaOptionsFactory,
apply_factory_meta_options, _missing)

# then we have to declare the meta options the meta options factory should support
class DebugMetaOption(MetaOption):
def __init__(self):
super().__init__(name='debug', default=False, inherit=True)

def check_value(self, value, mcs_args: McsArgs):
if not isinstance(value, bool):
raise TypeError(f'The {self.name} Meta option must be a bool')


class VerbosityMetaOption(MetaOption):
def __init__(self):
super().__init__(name='verbosity', default=1, inherit=True)

def check_value(self, value, mcs_args: McsArgs):
if value not in {1, 2, 3}:
raise ValueError(f'The {self.name} Meta option must either 1, 2, or 3')


class LogDestinationMetaOption(MetaOption):
def __init__(self):
super().__init__(name='log_destination', default=_missing, inherit=True)

# this pattern is useful if you need a mutable default value like [] or {}
def get_value(self, Meta, base_classes_meta, mcs_args: McsArgs):
value = super().get_value(Meta, base_classes_meta, mcs_args)
return value if value != _missing else 'stdout'

def check_value(self, value, mcs_args: McsArgs):
if value in {'stdout', 'stderr'}:
return

try:
valid_dir = os.path.exists(os.path.dirname(value))
except Exception:
valid_dir = False

if not valid_dir:
raise ValueError(f'The {self.name} Meta option must be one of `stdout`, '
'`stderr`, or a valid filepath')
```

The next step is to subclass `MetaOptionsFactory` and specify the `MetaOption` subclasses you want:

```python
class LoggingMetaOptionsFactory(MetaOptionsFactory):
_options = [
DebugMetaOption,
VerbosityMetaOption,
LogDestinationMetaOption,
]
```

Then you need a metaclass to actually apply the factory:

```python
class LoggingMetaclass(type):
def __new__(mcs, name, bases, clsdict):
mcs_args = McsArgs(mcs, name, bases, clsdict)
apply_factory_meta_options(mcs_args, LoggingMetaOptionsFactory)
return super().__new__(*mcs_args)
```

And lastly, create the public class, using the metaclass just defined:

```python
class YourLoggableService(metaclass=LoggingMetaclass):
def do_important_stuff(self):
if self.Meta.verbosity < 3:
self._log('doing important stuff')
else:
self._log('doing really detailed important stuff like so')

def _log(self, msg):
if not self.Meta.debug:
return

if self.Meta.log_destination == 'stdout':
print(msg)
elif self.Meta.log_destination == 'stderr':
sys.stderr.write(msg)
sys.stderr.flush()
else:
with open(self.Meta.log_destination, 'a') as f:
f.write(msg)
```

It's not immediately obvious from above, but the `Meta` attribute gets automatically added to classes having a metaclass that utilizes `apply_factory_meta_options`. (In this case, it will be populated with the default values as supplied by the `MetaOption` subclasses.) In the case where the class-under-construction (aka `YourLoggableService` in this example) has a partial `Meta` class, the missing meta options will be added to it.(*)

(*) In effect that's what happens, and for all practical purposes is probably how you should think about it, but technically speaking, the class-under-construction's `Meta` attribute actually gets replaced with a populated instance of the specified `MetaOptionsFactory` subclass.

The one thing we didn't cover is `MetaOption.contribute_to_class`. This is an optional callback hook that allows `MetaOption` subclasses to, well, contribute something to the class-under-construction. Maybe it adds/removes attributes to/from the class, or it wraps some method(s) with a decorator, or something else entirely.

## Included Metaclass Utilities

### Singleton

`Singleton` is an included metaclass that makes any class utilizing it a singleton:

```python
from py_meta_utils import Singleton


class YourSingleton(metaclass=Singleton):
pass


instance = YourSingleton()
assert instance == YourSingleton()
```

### SubclassableSingleton

`SubclassableSingleton` is an included metaclass that makes any class hierarchy utilizing it a singleton (you will always get the same instance of the most-derived subclass):

```python
from py_meta_utils import SubclassableSingleton


class BaseSingleton(metaclass=SubclassableSingleton):
pass


class Extended(BaseSingleton):
pass


base_instance = BaseSingleton()
extended_instance = Extended()
assert base_instance == extended_instance == BaseSingleton() == Extended()
```

Note that in practice, any subclasses must be imported *before* any calls to the base class(es) are made, otherwise you will not get the correct subclass.

### deep_getattr

`deep_getattr` acts just like `getattr` would on a constructed class object, except this operates on the pre-class-construction class dictionary and base classes. In other words, first we look for the attribute in the class dictionary, and then we search all the base classes (in method resolution order), finally returning the default value if the attribute was not found in any of the class dictionary or base classes (or it raises `AttributeError` if no default was given).

### OptionalMetaclass and OptionalClass

```python
try:
from optional_dependency import SomeClass
except ImportError:
from py_meta_utils import OptionalClass as SomeClass


class Optional(SomeClass):
pass
```

## License

MIT


Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

Py Meta Utils-0.4.0.tar.gz (9.3 kB view details)

Uploaded Source

Built Distribution

Py_Meta_Utils-0.4.0-py3-none-any.whl (8.0 kB view details)

Uploaded Python 3

File details

Details for the file Py Meta Utils-0.4.0.tar.gz.

File metadata

  • Download URL: Py Meta Utils-0.4.0.tar.gz
  • Upload date:
  • Size: 9.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.11.0 pkginfo/1.4.2 requests/2.19.1 setuptools/40.2.0 requests-toolbelt/0.8.0 tqdm/4.25.0 CPython/3.7.0

File hashes

Hashes for Py Meta Utils-0.4.0.tar.gz
Algorithm Hash digest
SHA256 413ec533c19b77f5c48288b902567907528a4ebc270df15e09f3e7461f9ae084
MD5 28b4c9201bf969fb7cd3e33240488d10
BLAKE2b-256 cfea1888bc5b8d1ed291c649c2502a7f085fe844b7f373fab921bfce462a2bd1

See more details on using hashes here.

File details

Details for the file Py_Meta_Utils-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: Py_Meta_Utils-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 8.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.11.0 pkginfo/1.4.2 requests/2.19.1 setuptools/40.2.0 requests-toolbelt/0.8.0 tqdm/4.25.0 CPython/3.7.0

File hashes

Hashes for Py_Meta_Utils-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c9917e1825f41b2f88fa1084942fd92052ab53df0f980bef6a9c2e2e3fcc8f3f
MD5 7856b31eac4d1ca108081d71fecb9052
BLAKE2b-256 f2cefc04efb7b043c182889d952d5097543e80891ace60b92ea3eab9d7b2de1d

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page