Changeset - 6a3d64ff2250
[Not reviewed]
0 3 0
Brett Smith - 4 years ago 2021-01-09 15:49:04
brettcsmith@brettcsmith.org
fields: Change FieldType capitalization.

This is friendlier to the YAML input and consistent with FieldFlags.
Less consistent with the rest of the codebase, but local consistency matters
more IMO.
3 files changed with 10 insertions and 11 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/pdfforms/fields.py
Show inline comments
...
 
@@ -8,138 +8,137 @@
 
import enum
 
import functools
 

	
 
from decimal import Decimal
 

	
 
import babel.numbers  # type:ignore[import]
 

	
 
from pdfminer.pdftypes import resolve1  # type:ignore[import]
 
from pdfminer import psparser  # type:ignore[import]
 
from . import utils as pdfutils
 
from .errors import PDFKeyError, PDFSpecError
 

	
 
from typing import (
 
    Any,
 
    Iterator,
 
    Optional,
 
    Mapping,
 
    MutableMapping,
 
    Sequence,
 
    Tuple,
 
    Union,
 
    cast,
 
)
 

	
 
FieldSource = MutableMapping[str, Any]
 

	
 
class FieldFlags(enum.IntFlag):
 
    # Flags for all fields
 
    ReadOnly = 2 ** 0
 
    Required = 2 ** 1
 
    NoExport = 2 ** 2
 
    # Flags for buttons
 
    NoToggleToOff = 2 ** 14
 
    Radio = 2 ** 15
 
    Pushbutton = 2 ** 16
 
    RadiosInUnison = 2 ** 25
 
    # Flags for text
 
    Multiline = 2 ** 12
 
    Password = 2 ** 13
 
    FileSelect = 2 ** 20
 
    DoNotSpellCheck = 2 ** 22
 
    DoNotScroll = 2 ** 23
 
    Comb = 2 ** 24
 
    RichText = 2 ** 25
 

	
 

	
 
class FieldType(enum.Enum):
 
    Btn = 'Btn'
 
    BUTTON = Btn
 
    Button = Btn
 
    Ch = 'Ch'
 
    CHOICE = Ch
 
    Choice = Ch
 
    Sig = 'Sig'
 
    SIG = Sig
 
    SIGNATURE = Sig
 
    Signature = Sig
 
    Tx = 'Tx'
 
    TEXT = Tx
 
    Text = Tx
 

	
 

	
 
class FormField:
 
    __slots__ = ['_source']
 
    _SENTINEL = object()
 
    DEFAULT_FILL: object = None
 
    INHERITABLE = frozenset([
 
        'DV',
 
        'Ff',
 
        'FT',
 
        'MaxLen',
 
        'Opt',
 
        'V',
 
    ])
 

	
 
    def __init__(self, source: FieldSource) -> None:
 
        self._source = source
 

	
 
    @classmethod
 
    def by_type(cls, source: FieldSource) -> 'FormField':
 
        retval = cls(source)
 
        try:
 
            field_type = retval.field_type()
 
        except ValueError:
 
            return retval
 
        flags = retval.flags()
 
        if field_type is FieldType.BUTTON:
 
        if field_type is FieldType.Button:
 
            if flags & FieldFlags.Radio:
 
                pass
 
            elif flags & FieldFlags.Pushbutton:
 
                pass
 
            else:
 
                retval.__class__ = CheckboxField
 
        elif field_type is FieldType.TEXT:
 
        elif field_type is FieldType.Text:
 
            retval.__class__ = TextField
 
        return retval
 

	
 
    def _get_value(self, key: str, default: Any=_SENTINEL) -> Any:
 
        can_inherit = key in self.INHERITABLE
 
        source: Optional[FieldSource] = self._source
 
        while source is not None:
 
            try:
 
                return resolve1(source[key])
 
            except KeyError:
 
                source = resolve1(source.get('Parent')) if can_inherit else None
 
        if default is self._SENTINEL:
 
            raise PDFKeyError(key)
 
        else:
 
            return default
 

	
 
    def field_type(self) -> FieldType:
 
        try:
 
            source = self._get_value('FT')
 
        except KeyError:
 
            raise PDFSpecError("field does not specify a field type") from None
 
        try:
 
            return FieldType[source.name]
 
        except (AttributeError, KeyError):
 
            raise PDFSpecError(f"field has invalid field type {source!r}") from None
 

	
 
    def kids(self) -> Iterator['FormField']:
 
        for source in self._get_value('Kids', ()):
 
            yield self.by_type(resolve1(source))
 

	
 
    def parent(self) -> Optional['FormField']:
 
        try:
 
            return self.by_type(self._get_value('Parent'))
 
        except KeyError:
 
            return None
 

	
 
    def add_kid(self, kid: 'FormField') -> None:
 
        if kid.parent() is not None:
 
            raise ValueError("given kid field already has a parent")
 
        kid._source['Parent'] = self._source
 
        self._source.setdefault('Kids', []).append(kid._source)
 

	
 
    def is_terminal(self) -> bool:
 
        return not self._get_value('Kids', None)
 

	
 
    def flags(self) -> int:
 
        return self._get_value('Ff', 0)  # type:ignore[no-any-return]
 

	
setup.py
Show inline comments
 
#!/usr/bin/env python3
 

	
 
from setuptools import setup
 

	
 
setup(
 
    name='conservancy_beancount',
 
    description="Plugin, library, and reports for reading Conservancy's books",
 
    version='1.15.0',
 
    version='1.15.1',
 
    author='Software Freedom Conservancy',
 
    author_email='info@sfconservancy.org',
 
    license='GNU AGPLv3+',
 

	
 
    install_requires=[
 
        'babel>=2.6',  # Debian:python3-babel
 
        'beancount>=2.2',  # Debian:beancount
 
        'GitPython>=2.0',  # Debian:python3-git
 
        # 1.4.1 crashes when trying to save some documents.
 
        'odfpy>=1.4.0,!=1.4.1',  # Debian:python3-odf
 
        'pdfminer.six>=20200101',
 
        'PyYAML>=3.0',  # Debian:python3-yaml
 
        'regex',  # Debian:python3-regex
 
        'rt>=2.0',
 
    ],
 
    setup_requires=[
 
        'pytest-mypy',
 
        'pytest-runner',  # Debian:python3-pytest-runner
 
    ],
 
    tests_require=[
 
        'mypy>=0.770',  # Debian:python3-mypy
 
        'pytest',  # Debian:python3-pytest
 
    ],
 

	
 
    packages=[
 
        'conservancy_beancount',
 
        'conservancy_beancount.pdfforms',
 
        'conservancy_beancount.plugin',
 
        'conservancy_beancount.reports',
 
        'conservancy_beancount.tools',
 
    ],
 
    entry_points={
 
        'console_scripts': [
 
            'accrual-report = conservancy_beancount.reports.accrual:entry_point',
 
            'assemble-audit-reports = conservancy_beancount.tools.audit_report:entry_point',
 
            'balance-sheet-report = conservancy_beancount.reports.balance_sheet:entry_point',
 
            'budget-report = conservancy_beancount.reports.budget:entry_point',
 
            'bean-sort = conservancy_beancount.tools.sort_entries:entry_point',
 
            'extract-odf-links = conservancy_beancount.tools.extract_odf_links:entry_point',
 
            'fund-report = conservancy_beancount.reports.fund:entry_point',
 
            'ledger-report = conservancy_beancount.reports.ledger:entry_point',
 
            'opening-balances = conservancy_beancount.tools.opening_balances:entry_point',
 
            'pdfform-extract = conservancy_beancount.pdfforms.extract:entry_point',
 
            'pdfform-fill = conservancy_beancount.pdfforms.fill:entry_point',
 
            'split-ods-links = conservancy_beancount.tools.split_ods_links:entry_point',
 
        ],
 
    },
 
)
tests/test_pdfforms_fields.py
Show inline comments
...
 
@@ -20,146 +20,146 @@ def field_source(
 
        name=None,
 
        value=None,
 
        field_type=None,
 
        flags=None,
 
        parent=None,
 
        kids=None,
 
        *,
 
        literal=None,
 
):
 
    retval = {}
 
    if isinstance(name, str):
 
        retval['T'] = name.encode('ascii')
 
    elif name is not None:
 
        retval['T'] = name
 
    if value is not None:
 
        if literal is None:
 
            literal = field_type and field_type != 'Tx'
 
        if literal:
 
            value = PSLiteral(value)
 
        retval['V'] = value
 
    if field_type is not None:
 
        retval['FT'] = PSLiteral(field_type)
 
    if flags is not None:
 
        retval['Ff'] = flags
 
    if parent is not None:
 
        retval['Parent'] = parent
 
    if kids is not None:
 
        retval['Kids'] = list(kids)
 
    return retval
 

	
 
def appearance_states(*names):
 
    return {key: object() for key in names if key is not None}
 

	
 
def test_empty_field():
 
    source = field_source()
 
    field = fieldsmod.FormField(source)
 
    assert not field.name()
 
    assert field.value() is None
 
    assert field.parent() is None
 
    assert not list(field.kids())
 
    assert field.flags() == 0
 
    assert field.is_terminal()
 
    with pytest.raises(ValueError):
 
        field.field_type()
 

	
 
def test_text_field_base():
 
    source = field_source(b's', b'string of text', 'Tx')
 
    field = fieldsmod.FormField(source)
 
    assert field.field_type() is fieldsmod.FieldType.TEXT
 
    assert field.field_type() is fieldsmod.FieldType.Text
 
    assert field.name() == 's'
 
    assert field.value() == b'string of text'
 

	
 
@pytest.mark.parametrize('value', ['Off', 'Yes', 'On'])
 
def test_checkbox_field_base(value):
 
    source = field_source(b'cb', value, 'Btn', literal=True)
 
    field = fieldsmod.FormField(source)
 
    assert field.field_type() is fieldsmod.FieldType.BUTTON
 
    assert field.field_type() is fieldsmod.FieldType.Button
 
    assert field.name() == 'cb'
 
    assert field.value().name == value
 

	
 
@pytest.mark.parametrize('flags', range(4))
 
def test_readonly_flag(flags):
 
    source = field_source(flags=flags)
 
    field = fieldsmod.FormField(source)
 
    assert field.flags() == flags
 
    assert field.is_readonly() == flags % 2
 

	
 
@pytest.mark.parametrize('kid_count', range(3))
 
def test_kids(kid_count):
 
    kids = [field_source(f'kid{n}', field_type='Ch') for n in range(kid_count)]
 
    source = field_source(kids=iter(kids))
 
    field = fieldsmod.FormField(source)
 
    got_kids = list(field.kids())
 
    assert len(got_kids) == len(kids)
 
    assert field.is_terminal() == (not kids)
 
    for actual, expected in zip(got_kids, kids):
 
        assert actual.name() == expected['T'].decode('ascii')
 

	
 
def test_kids_by_type():
 
    kids = [field_source(field_type='Tx'), field_source(field_type='Btn')]
 
    source = field_source('topform', kids=iter(kids))
 
    actual = fieldsmod.FormField.by_type(source).kids()
 
    assert isinstance(next(actual), fieldsmod.TextField)
 
    assert isinstance(next(actual), fieldsmod.CheckboxField)
 
    assert next(actual, None) is None
 

	
 
def test_inheritance():
 
    parent_source = field_source(b'parent', 'parent value', 'Tx', 17)
 
    kid_source = field_source('kid', parent=parent_source)
 
    parent_source['Kids'] = [kid_source]
 
    field = fieldsmod.FormField(kid_source)
 
    parent = field.parent()
 
    assert parent is not None
 
    assert parent.name() == 'parent'
 
    assert not parent.is_terminal()
 
    assert field.is_terminal()
 
    assert field.name() == 'kid'
 
    assert field.field_type() is fieldsmod.FieldType.TEXT
 
    assert field.field_type() is fieldsmod.FieldType.Text
 
    assert field.value() == 'parent value'
 
    assert field.flags() == 17
 
    assert not list(field.kids())
 

	
 
@pytest.mark.parametrize('field_type,value', [
 
    ('Tx', b'new value'),
 
    ('Btn', PSLiteral('Yes')),
 
])
 
def test_set_value(field_type, value):
 
    source = field_source(field_type=field_type)
 
    field = fieldsmod.FormField(source)
 
    assert field.value() is None
 
    field.set_value(value)
 
    assert field.value() == value
 

	
 
@pytest.mark.parametrize('field_type,expected', [
 
    ('Tx', fieldsmod.TextField),
 
    ('Btn', fieldsmod.CheckboxField),
 
])
 
def test_by_type(field_type, expected):
 
    source = field_source(field_type=field_type)
 
    field = fieldsmod.FormField.by_type(source)
 
    assert isinstance(field, expected)
 

	
 
def test_container_by_type():
 
    kids = [field_source(field_type='Tx'), field_source(field_type='Btn')]
 
    source = field_source('topform', kids=iter(kids))
 
    field = fieldsmod.FormField.by_type(source)
 
    assert isinstance(field, fieldsmod.FormField)
 

	
 
@pytest.mark.parametrize('flag', [
 
    # If you add dedicated classes for these types of buttons, you can remove
 
    # their test cases.
 
    fieldsmod.FieldFlags.Radio,
 
    fieldsmod.FieldFlags.Pushbutton,
 
])
 
def test_unsupported_button_by_type(flag):
 
    source = field_source(field_type='Btn', flags=flag)
 
    field = fieldsmod.FormField.by_type(source)
 
    assert type(field) is fieldsmod.FormField
 

	
 
@pytest.mark.parametrize('field_type', [
 
    # If you add dedicated classes for these types of fields, you can remove
 
    # their test cases.
 
    'Ch',
 
    'Sig',
 
])
 
def test_unsupported_field_by_type(field_type):
0 comments (0 inline, 0 general)