Skip to content

Commit

Permalink
Initial plugin system (#39)
Browse files Browse the repository at this point in the history
* Renamed pyfdl.py to fdl to match contents
* Switched load(s) and dump(a) to new read_from_file/string and write_to_file/string functions
* Reading and Writing of FDL files is now done through a built-in FDLHandler
* Unittests updated
* Examples updated
* A bit of future proofing. Entry points can either be pointing to a register function inside the module or it will look for one.
* Make sure built-in handlers have a "register_plugin" function
* Added a cleanup of temp files fixture to clean up NamedTempFiles
* Dummy plugin for testing
* Toggle memory flag for list of pages
* Import yaml in test env
* added pyyaml to pip installs. Will replace with hatch calls later

Signed-off-by: apetrynet <flehnerheener@gmail.com>

---------

Signed-off-by: apetrynet <flehnerheener@gmail.com>
  • Loading branch information
apetrynet authored Aug 18, 2024
1 parent d2938f5 commit c0c08a1
Show file tree
Hide file tree
Showing 36 changed files with 831 additions and 127 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test_pyfdl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest mktestdocs
python -m pip install flake8 pytest mktestdocs pyyaml
- name: Install PyFDL
run: |
pip install .
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 4 additions & 0 deletions docs/Handlers/handlers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Handlers

## FDLHandler
This is the built-in handler for reading and writing fdl files
113 changes: 113 additions & 0 deletions docs/Plugins/plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# About Plugins

PyFDL supports plugins for expanding the toolkit.
This may be useful to add support for converting frame line definitions in another formats to FDL or
reading metadata from files to mention a few examples.

## Plugin types

### Handlers
Plugins that take care of reading/writing files to and/or from FDL are called `handlers`.
PyFDL comes with a built-in [`FDLHandler`](../Handlers/handlers.md#fdlhandler) which takes care of reading and writing
FDL files.

## Writing a handler plugin
There are only a couple of requirements for a handler.
1. The handler needs to be a class with a `name` variable. If your handler deals with files you should
provide a `suffixes` variable containing a list of suffixes to support. Suffixes include the dot like. ".yml"
2. The module needs to provide a function, example: `register_plugin(registry)` which accepts one argument for
the registry. This function will call `registry.add_handler(<HANDLER_INSTANCE>)` with an instance of your
handler.

Example of a YAML Handler:
```python
import yaml
from pathlib import Path
from typing import Optional, Any
from pyfdl import FDL

class YAMLHandler:
def __init__(self):
# Name is required
self.name = 'yaml'
# Suffixes may be used to automagically select this handler based on path
self.suffixes = [".yml", ".yaml"]

def write_to_string(self, fdl: FDL, validate: bool = True, **yaml_args: Optional[Any]) -> str:
if validate:
fdl.validate()

return yaml.dump(fdl.to_dict(), **yaml_args)

def write_to_file(self, fdl: FDL, path: Path, validate: bool = True, **yaml_args: Optional[Any]) -> str:
if validate:
fdl.validate()
print('yoho')
with path.open('w') as f:
f.write(yaml.dump(fdl.to_dict(), **yaml_args))

def custom_method(self, fdl: FDL, brand: str) -> FDL:
fdl.fdl_creator = brand
return fdl


def register_plugin(registry: 'PluginRegistry'):
registry.add_handler(YAMLHandler())
```

## Installing a plugin
### Install via pip
Ideally you package your plugin according to best practices and share it via PyPi for other to use.
However, the only requirement from PyFDL's perspective is that you install the plugin to PyFDL's
plugin namespace: `pyfdl.plugins`.

In your pyproject.toml or setup.py make sure to add your package/module like so:
``` toml

[project.entry-points."pyfdl.plugins"]
# If you choose to call your register function something else, make sure to adjust the entry below.
yaml_handler = "yaml_handler:register_plugin"
```

You are free to name your register function whatever you like, but make sure you add it after the module name
If you don't provide a function name PyFDL will assume the function is named `register_plugin` and will
ignore your plugin if that is not the case.

### Install at runtime
You may also add your handler directly in your code.
To do this, simply get a hold of the registry and add your handler.

```python
from pyfdl.plugins import get_registry

registry = get_registry()
registry.add_handler(YAMLHandler())
```

## Using your handler
If your handler provides one or more of the following methods: `read_from_file` `read_from_string`,
`write_to_file` or `write_to_string`, PyFDL will choose your handler based on either path (suffix) or
directly asking for this specific handler.

```python
import pyfdl
from pathlib import Path
from tempfile import NamedTemporaryFile

fdl = pyfdl.FDL()
fdl.apply_defaults()
pyfdl.write_to_string(fdl, handler_name='yaml', indent=4)
pyfdl.write_to_file(fdl, path=NamedTemporaryFile(suffix='.yml', delete=False).name)
```
If your handler doesn't provide one of the methods above or you have others exposed you can use them like so:
```python
from pyfdl.handlers import get_handler

my_handler = get_handler(func_name='custom_method', handler_name='yaml')
fdl = FDL()
fdl.apply_defaults()
assert fdl.fdl_creator == 'PyFDL'
my_handler.custom_method(fdl, "my brand")
assert fdl.fdl_creator == 'my brand'

```
8 changes: 8 additions & 0 deletions docs/Plugins/registry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Plugin Registry
The `PluginRegistry` is in charge of loading and keeping track of PyFDL's plugins.

____

::: pyfdl.plugins.registry
options:
inherited_members: false
25 changes: 12 additions & 13 deletions docs/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ round values of dimensions accordingly.
A canvas+framing decision for a "raw" camera canvas should in theory keep more precision than a
canvas+framing decision for a conformed VFX plate.

The rules for rounding strategy are the same as for [CanvasTemplate.round](Classes/common.md#pyfdl.RoundStrategy)
The rules for rounding strategy are the same as for [CanvasTemplate.round](FDL Classes/common.md#pyfdl.RoundStrategy)


The [default](Classes/common.md#pyfdl.DEFAULT_ROUNDING_STRATEGY) strategy is to round dimensions to
The [default](FDL Classes/common.md#pyfdl.DEFAULT_ROUNDING_STRATEGY) strategy is to round dimensions to
even numbers, but this may be overridden by setting the rounding strategy to
[`NO_ROUNDING`](Classes/common.md#pyfdl.NO_ROUNDING)
[`NO_ROUNDING`](FDL Classes/common.md#pyfdl.NO_ROUNDING)

Here are a couple examples of setting the rounding strategy:
```python
Expand All @@ -42,7 +42,7 @@ fdl.set_rounding_strategy({'even': 'whole', 'mode': 'up'})
```python
import pyfdl
from pyfdl import Canvas, FramingIntent, Dimensions, Point
from io import StringIO
from tempfile import NamedTemporaryFile

fdl = pyfdl.FDL()

Expand Down Expand Up @@ -80,20 +80,19 @@ fdl.place_canvas_in_context(context_label="PanavisionDXL2", canvas=canvas)
# Finally, let's create a framing decision
canvas.place_framing_intent(framing_intent=framing_intent)

# Validate our FDL and save it (using StringIO as example)
with StringIO() as f:
pyfdl.dump(fdl, f, validate=True)
# Validate our FDL and save it
with NamedTemporaryFile(suffix='.fdl', delete=False) as f:
pyfdl.write_to_file(fdl, f.name, validate=True)
```

### Create a Canvas from a Canvas Template
```python
import pyfdl
from io import StringIO
from pathlib import Path
from tempfile import NamedTemporaryFile

fdl_file = Path('tests/sample_data/Scenario-9__OriginalFDL_UsedToMakePlate.fdl')
with fdl_file.open('r') as f:
fdl = pyfdl.load(f)
fdl = pyfdl.read_from_file(fdl_file)

# Select the first canvas in the first context
context = fdl.contexts[0]
Expand All @@ -113,7 +112,7 @@ new_canvas = pyfdl.Canvas.from_canvas_template(
# Place the new canvas along side the source
fdl.place_canvas_in_context(context_label=context.label, canvas=new_canvas)

# Validate and "save"
with StringIO() as f:
pyfdl.dump(fdl, f, validate=True)
# Validate and write to file.
with NamedTemporaryFile(suffix='.fdl', delete=False) as f:
pyfdl.write_to_file(fdl, f.name, validate=True)
```
8 changes: 4 additions & 4 deletions docs/top_level_functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
These functions follow the convention of other file parsing packages like python's builtin json package
and provide both reading and writing to and from strings and objects.

::: pyfdl.load
::: pyfdl.read_from_file

::: pyfdl.loads
::: pyfdl.read_from_string

::: pyfdl.dump
::: pyfdl.write_to_file

::: pyfdl.dumps
::: pyfdl.write_to_string
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ path = "src/pyfdl/__init__.py"
[tool.hatch.envs.test]
dependencies = [
"pytest",
"mktestdocs"
"mktestdocs",
"pyyaml"
]

[tool.hatch.envs.test.scripts]
Expand Down
81 changes: 7 additions & 74 deletions src/pyfdl/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import json

from typing import IO, Union

from .common import (
Base,
Dimensions,
Expand All @@ -20,8 +16,9 @@
from .canvas import Canvas
from .context import Context
from .canvas_template import CanvasTemplate
from .pyfdl import FDL
from .fdl import FDL
from .errors import FDLError, FDLValidationError
from .handlers import read_from_file, read_from_string, write_to_string, write_to_file

__all__ = [
'Base',
Expand All @@ -39,78 +36,14 @@
'FramingDecision',
'FramingIntent',
'Header',
'load',
'loads',
'NO_ROUNDING',
'Point',
'read_from_file',
'read_from_string',
'RoundStrategy',
'TypedCollection'
'TypedCollection',
'write_to_file',
'write_to_string'
]

__version__ = "0.1.0.dev0"


def load(fp: IO, validate: bool = True) -> FDL:
"""
Load an FDL from a file.
Args:
fp: file pointer
validate: validate incoming json with jsonschema
Raises:
jsonschema.exceptions.ValidationError: if the contents doesn't follow the spec
Returns:
FDL:
"""
raw = fp.read()
return loads(raw, validate=validate)


def loads(s: str, validate: bool = True) -> FDL:
"""Load an FDL from string.
Args:
s: string representation of an FDL
validate: validate incoming json with jsonschema
Returns:
FDL:
"""
fdl = FDL.from_dict(json.loads(s))

if validate:
fdl.validate()

return fdl


def dump(obj: FDL, fp: IO, validate: bool = True, indent: Union[int, None] = 2):
"""Dump an FDL to a file.
Args:
obj: object to serialize
fp: file pointer
validate: validate outgoing json with jsonschema
indent: amount of spaces
"""
fp.write(dumps(obj, validate=validate, indent=indent))


def dumps(obj: FDL, validate: bool = True, indent: Union[int, None] = 2) -> str:
"""Dump an FDL to string
Args:
obj: object to serialize
validate: validate outgoing json with jsonschema
indent: amount of spaces
Returns:
string: representation of the resulting json
"""
if validate:
obj.validate()

return json.dumps(obj.to_dict(), indent=indent, sort_keys=False)
8 changes: 8 additions & 0 deletions src/pyfdl/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,11 @@ class FDLError(Exception):

class FDLValidationError(FDLError):
pass


class HandlerError(FDLError):
pass


class UnknownHandlerError(HandlerError):
pass
File renamed without changes.
Loading

0 comments on commit c0c08a1

Please sign in to comment.