Skip to content

Commit

Permalink
test: Run mypy as part of automated tests
Browse files Browse the repository at this point in the history
Add attribute type annotations where required and include mypy as part
of the standard automated tests.

Signed-off-by: Michael Brown <mbrown@fensystems.co.uk>
  • Loading branch information
mcb30 committed Feb 19, 2020
1 parent c6373f2 commit 6905594
Show file tree
Hide file tree
Showing 13 changed files with 92 additions and 58 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
__pycache__
.eggs/
.mypy_cache/
__pycache__/
build/
dist/
*.pyc
Expand Down
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ python:
install:
- pip install -e .
- pip install coverage
- pip install mypy
- pip install pycodestyle
- pip install pylint
script: ./test.sh
Expand Down
52 changes: 27 additions & 25 deletions idiosync/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io
import itertools
import sys
from typing import ClassVar, Type
import uuid
import weakref

Expand Down Expand Up @@ -41,7 +42,7 @@ def __repr__(self):
def __str__(self):
return str(self.key)

db = None
db: ClassVar['Database'] = None
"""Containing user database
This is populated as a class attribute when the containing
Expand Down Expand Up @@ -155,10 +156,10 @@ def __repr__(self):
class State(MutableMapping):
"""User database synchronization state"""

KEY_LEN = 128
KEY_LEN: ClassVar[int] = 128
"""Maximum state key length"""

KEY_COOKIE = 'cookie'
KEY_COOKIE: ClassVar[str] = 'cookie'
"""Synchronization cookie state key"""

def __init__(self, db):
Expand Down Expand Up @@ -236,31 +237,30 @@ def __init__(self, **kwargs):
self.options = kwargs


ConfigType = Type[Config]
UserType = Type[User]
GroupType = Type[Group]


class Database(ABC):
"""A user database"""

Config: ClassVar[ConfigType]
"""Configuration class for this database"""

User: ClassVar[UserType]
"""User class for this database"""

Group: ClassVar[GroupType]
"""Group class for this database"""

def __init__(self, **kwargs):
self.config = self.Config(**kwargs)
self.config = self.Config(**kwargs) # pylint: disable=no-member
# Construct User and Group classes attached to this database
db = weakref.proxy(self)
self.User = type(self.User.__name__, (self.User,), {'db': db})
self.Group = type(self.Group.__name__, (self.Group,), {'db': db})

@property
@abstractmethod
def Config(self):
"""Configuration class for this database"""

@property
@abstractmethod
def User(self):
"""User class for this database"""

@property
@abstractmethod
def Group(self):
"""Group class for this database"""

def user(self, key):
"""Look up user"""
return self.User.find(key)
Expand Down Expand Up @@ -320,17 +320,19 @@ def trace(self, fh=None, cookiefh=None, **kwargs):
cookiefh.flush()


StateType = Type[State]


class WritableDatabase(Database):
"""A writable user database"""

State: ClassVar[StateType]
"""State class for this database"""

def __init__(self, **kwargs):
super().__init__(**kwargs)
self.state = self.State(weakref.proxy(self))

@property
@abstractmethod
def State(self):
"""State class for this database"""
db = weakref.proxy(self)
self.state = self.State(db) # pylint: disable=no-member

def find_syncids(self, syncids, invert=False):
"""Look up user database entries by synchronization identifier"""
Expand Down
12 changes: 9 additions & 3 deletions idiosync/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
import argparse
from contextlib import nullcontext
import logging
from typing import ClassVar, List, Type
from .config import Config, DatabaseConfig, SynchronizerConfig


class Command(ABC):
"""An executable command"""

loglevels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
loglevels: ClassVar[List] = [logging.ERROR, logging.WARNING,
logging.INFO, logging.DEBUG]

def __init__(self, argv=None):
self.args = self.parser().parse_args(argv)
Expand Down Expand Up @@ -42,15 +44,19 @@ def main(cls):
cls().execute()


ConfigType = Type[Config]


class ConfigCommand(Command):
"""An executable command utilising a configuration file"""
# pylint: disable=abstract-method

Config = Config
Config: ClassVar[ConfigType]

def __init__(self, argv=None):
super().__init__(argv)
self.config = self.Config.load(self.args.config)
filename = self.args.config
self.config = self.Config.load(filename) # pylint: disable=no-member

@classmethod
def parser(cls, **kwargs):
Expand Down
13 changes: 9 additions & 4 deletions idiosync/config.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Configuration files"""

from abc import ABC, abstractmethod
from typing import ClassVar, Type
import yaml
from .plugins import plugins
from .sync import DatabaseSynchronizer
from .sync import Synchronizer


class ConfigError(Exception):
Expand Down Expand Up @@ -59,11 +60,15 @@ def database(self):
return plugins[self.plugin](**self.params)


DatabaseConfigType = Type[DatabaseConfig]
SynchronizerType = Type[Synchronizer]


class SynchronizerConfig(Config):
"""A database synchronizer configuration"""

DatabaseConfig = DatabaseConfig
DatabaseSynchronizer = DatabaseSynchronizer
DatabaseConfig: ClassVar[DatabaseConfigType] = DatabaseConfig
Synchronizer: ClassVar[SynchronizerType] = Synchronizer

def __init__(self, src, dst):
self.src = src
Expand All @@ -88,4 +93,4 @@ def parse(cls, config):
@property
def synchronizer(self):
"""Configured synchronizer"""
return self.DatabaseSynchronizer(self.src.database, self.dst.database)
return self.Synchronizer(self.src.database, self.dst.database)
8 changes: 4 additions & 4 deletions idiosync/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@ class LdapResponseControl(ldap.controls.ResponseControl):
LDAP control.
"""

RE = re.compile(
RE: ClassVar[re.Pattern] = re.compile(
r'(?P<control>\S+)\s+(?P<criticality>true|false)\s+(?P<value>\S+)'
)

CRITICALITY = {
CRITICALITY: ClassVar[Dict[str, bool]] = {
'true': True,
'false': False,
}
Expand Down Expand Up @@ -130,7 +130,7 @@ class LdapResult(TraceEvent):
name: str = None
value: bytes = None

RE: ClassVar = re.compile(
RE: ClassVar[re.Pattern] = re.compile(
r'#\s+((result:\s+(?P<result>\d+))|(control:\s+(?P<control>.*)))'
)

Expand Down Expand Up @@ -298,7 +298,7 @@ class LdapEntry(Entry):
memberOf = LdapStringAttribute('memberOf', multi=True)
uuid = LdapEntryUuidAttribute('entryUUID')

model = None
model: ClassVar[LdapModel] = None
"""LDAP model"""

def __init__(self, dn, attrs):
Expand Down
10 changes: 5 additions & 5 deletions idiosync/mediawiki.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy
from .sqlalchemy import (BinaryString, UnsignedInteger, UuidChar, SqlModel,
SqlAttribute, SqlUser, SqlSyncId, SqlStateModel,
SqlState, SqlConfig, SqlDatabase)
SqlAttribute, SqlUser, SqlGroup, SqlSyncId,
SqlStateModel, SqlState, SqlConfig, SqlDatabase)
from .dummy import DummyGroup

##############################################################################
Expand Down Expand Up @@ -171,7 +171,7 @@ def groups(self):
return (self.db.Group(x.ug_group) for x in self.row.user_groups)


class MediaWikiGroup(DummyGroup):
class MediaWikiGroup(DummyGroup, SqlGroup):
"""A MediaWiki group
The MediaWiki database has no table for group definitions: groups
Expand All @@ -197,8 +197,8 @@ class MediaWikiState(SqlState):
class MediaWikiConfig(SqlConfig):
"""MediaWiki user database configuration"""

def __init__(self, title_case=True, **kwargs):
super().__init__(**kwargs)
def __init__(self, uri, title_case=True, **kwargs):
super().__init__(uri, **kwargs)
self.title_case = title_case


Expand Down
11 changes: 6 additions & 5 deletions idiosync/sqlalchemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from abc import ABCMeta
import logging
from typing import ClassVar
import uuid
from sqlalchemy import create_engine, inspect, and_
from sqlalchemy.orm import sessionmaker, contains_eager
Expand Down Expand Up @@ -183,10 +184,10 @@ def __init__(cls, name, bases, dct):
class SqlEntry(WritableEntry, metaclass=SqlEntryMeta):
"""A SQL user database entry"""

model = None
model: ClassVar[SqlModel] = None
"""SQLAlchemy model for this table"""

uuid_ns = None
uuid_ns: ClassVar[uuid.UUID] = None
"""UUID namespace for entries within this table"""

def __init__(self, row):
Expand Down Expand Up @@ -267,7 +268,7 @@ def delete(self):
def prepare(cls):
"""Prepare for use as part of an idiosync user database"""
# Create SyncId column if needed
if cls.model.syncid is not None:
if cls.model is not None and cls.model.syncid is not None:
attr = getattr(cls.model.orm, cls.model.syncid)
desc = inspect(cls.model.orm).all_orm_descriptors[cls.model.syncid]
if desc.extension_type is ASSOCIATION_PROXY:
Expand Down Expand Up @@ -304,7 +305,7 @@ def users(self):
class SqlSyncId:
"""Synchronization identifier mixin"""

__syncid__ = None
__syncid__: ClassVar[str] = None

def __init__(self, syncid=None, **kwargs):
"""Allow row to be constructed from a synchronization identifier"""
Expand All @@ -330,7 +331,7 @@ def __init__(self, orm, key, value):
class SqlState(State):
"""SQL user database synchronization state"""

model = None
model: ClassVar[SqlStateModel] = None
"""SQLAlchemy synchronization state model"""

def __init__(self, db):
Expand Down
26 changes: 16 additions & 10 deletions idiosync/sync.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""User database synchronization"""

from abc import ABC, abstractmethod
from abc import ABC
import logging
from typing import ClassVar, List, Type
from .base import (Entry, User, SyncCookie, SyncId, SyncIds, UnchangedSyncIds,
DeletedSyncIds, RefreshComplete)

Expand Down Expand Up @@ -57,6 +58,9 @@ def sync_single_to_single(self, src, dst):
class EntrySynchronizer(ABC):
"""A user database entry synchronizer"""

attrs: List[str]
"""Attribute list"""

def __init__(self, Src, Dst):

# Record source and destination classes
Expand All @@ -75,11 +79,6 @@ def __repr__(self):
return "%s(%s,%s)" % (self.__class__.__name__, self.Src.__name__,
self.Dst.__name__)

@property
@abstractmethod
def attrs(self):
"""Attribute list"""

def sync(self, src, dst):
"""Synchronize entries"""

Expand Down Expand Up @@ -111,11 +110,15 @@ class GroupSynchronizer(EntrySynchronizer):
attrs = ['commonName', 'description']


class DatabaseSynchronizer:
UserSynchronizerType = Type[UserSynchronizer]
GroupSynchronizerType = Type[GroupSynchronizer]


class Synchronizer:
"""A user database synchronizer"""

UserSynchronizer = UserSynchronizer
GroupSynchronizer = GroupSynchronizer
UserSynchronizer: ClassVar[UserSynchronizerType] = UserSynchronizer
GroupSynchronizer: ClassVar[GroupSynchronizerType] = GroupSynchronizer

def __init__(self, src, dst):
self.src = src
Expand Down Expand Up @@ -225,6 +228,9 @@ def sync(self, persist=True, strict=False, delete=False):
raise TypeError(src)


SynchronizerType = Type[Synchronizer]


def synchronize(src, dst, **kwargs):
"""Synchronize source database to destination database"""
DatabaseSynchronizer(src, dst).sync(**kwargs)
Synchronizer(src, dst).sync(**kwargs)
2 changes: 1 addition & 1 deletion idiosync/test/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class SynchronizerTestCase(ReplayTestCase):
"""Synchronization test case base class"""
# pylint: disable=too-many-public-methods

plugin = None
plugin: str = None

def setUp(self):
super().setUp()
Expand Down
4 changes: 4 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[mypy]
strict_optional = False
ignore_missing_imports = True
plugins = sqlmypy
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
] + ([] if os.getenv('READTHEDOCS') else [
'python-ldap',
])),
tests_require=([
'sqlalchemy-stubs',
]),
entry_points={
'console_scripts': [
'idiosync=idiosync.cli:SynchronizeCommand.main',
Expand Down

0 comments on commit 6905594

Please sign in to comment.