Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an API and a component architecture based on metaclasses #1

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,6 @@ venv.bak/

# mypy
.mypy_cache/

# vim
*.swp
8 changes: 8 additions & 0 deletions repository/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
__version__ = 0.2

# from pkgutil import extend_path
# __path__ = extend_path(__path__, __name__)

import repository.api
import repository.core
import repository.vendors
61 changes: 61 additions & 0 deletions repository/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@

from .component import Interface


class IRepository(Interface):

"""Common repository interface"""

def __init__(self, url, settings={}, debug=False):
"""Allocate internal resources and reset state, so that this repository is
ready for a new run.
"""

@staticmethod
def id():
"""Short alphanumeric, lower-case string which uniquely identify this
repository, suitable for use as an HTTP/GET argument value, in filenames,
etc..."""

# implementation: only letters and digits are allowed. An exception will
# be raised by MetaRepository if the id is malformed or not unique amongst
# registered repositories.

@staticmethod
def name():
"""Name of the repository with any character"""

def get_repository_instance(self):
# Vendor instance to act on the repository
""" """

def get_host_instance(self):
# Vendor instance to act on the host
"""" """

def get_readme(self):
""" """

def get_summary(self):
""" """

def get_latest_commits(self):
""" """

def get_latest_tags(self):
""" """

def get_archive_url(self, **kwargs):
""" """

def get_commits_contributors(self):
""" """

def get_issues_contributors(self):
""" """

def get_members(self):
""" """

def get_languages(self):
""" """
159 changes: 159 additions & 0 deletions repository/component.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2003-2018 Edgewall Software
# Copyright (C) 2003-2004 Jonas Borgström <[email protected]>
# Copyright (C) 2004-2005 Christopher Lenz <[email protected]>
# Copyright (c) 2009 Olivier Guilyardi <[email protected]>

# All rights reserved.
# This file is part of TimeSide.

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.


# This file defines a generic object interface mechanism and
# a way to determine which components implements a given interface.
#
# For example, the following defines the Music class as implementing the
# listenable interface.
#
# class Listenable(Interface):
# pass
#
# class Music(Component):
# implements(Listenable)
#
# Several class can implements a such interface, and it is possible to
# discover which class implements it with implementations():
#
# list_of_classes = implementations(Listenable)
#
# This mechanism support inheritance of interfaces: a class implementing a given
# interface is also considered to implement all the ascendants of this interface.
#
# However, inheritance is not supported for components. The descendants of a class
# implementing a given interface are not automatically considered to implement this
# interface too.

__all__ = ['Component', 'MetaComponent', 'implements', 'abstract',
'interfacedoc', 'Interface', 'implementations', 'ComponentError']



class Interface(object):

"""Marker base class for interfaces."""


def implements(*interfaces):
"""Registers the interfaces implemented by a component when placed in the
class header"""
MetaComponent.implements.extend(interfaces)


def abstract():
"""Declare a component as abstract when placed in the class header"""
MetaComponent.abstract = True


def implementations(interface, recurse=True, abstract=False):
"""Returns the components implementing interface, and if recurse, any of
the descendants of interface. If abstract is True, also return the
abstract implementations."""
result = []
find_implementations(interface, recurse, abstract, result)
return result


def interfacedoc(func):
if isinstance(func, staticmethod):
raise ComponentError(
"@interfacedoc can't handle staticmethod (try to put @staticmethod above @interfacedoc)")

if not func.__doc__:
func.__doc__ = "@interfacedoc"
func._interfacedoc = True
return func


class MetaComponent(type):

"""Metaclass of the Component class, used mainly to register the interface
declared to be implemented by a component."""

implementations = []
implements = []
abstract = False

def __new__(cls, name, bases, d):
new_class = super(MetaComponent, cls).__new__(cls, name, bases, d)
# Register implementations
if MetaComponent.implements:
for i in MetaComponent.implements:
MetaComponent.implementations.append({
'interface': i,
'class': new_class,
'abstract': MetaComponent.abstract})

# Propagate @interfacedoc
for name in new_class.__dict__:
member = new_class.__dict__[name]
if isinstance(member, staticmethod):
member = getattr(new_class, name)

if member.__doc__ == "@interfacedoc":
if_member = None
for i in MetaComponent.implements:
if hasattr(i, name):
if_member = getattr(i, name)
if not if_member:
raise ComponentError("@interfacedoc: %s.%s: no such member in implemented interfaces: %s"
% (new_class.__name__, name, str(MetaComponent.implements)))
member.__doc__ = if_member.__doc__

MetaComponent.implements = []
MetaComponent.abstract = False

return new_class


class Component(object, metaclass=MetaComponent):

"""Base class of all components"""
pass

def extend_unique(list1, list2):
"""Extend list1 with list2 as list.extend(), but doesn't append duplicates
to list1"""
for item in list2:
if item not in list1:
list1.append(item)


def find_implementations(interface, recurse, abstract, result):
"""Find implementations of an interface or of one of its descendants and
extend result with the classes found."""
for item in MetaComponent.implementations:
if (item['interface'] == interface and (abstract or not item['abstract'])):
extend_unique(result, [item['class']])

if recurse:
subinterfaces = interface.__subclasses__()
if subinterfaces:
for i in subinterfaces:
find_implementations(i, recurse, abstract, result)


class ComponentError(Exception):
pass
73 changes: 73 additions & 0 deletions repository/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import os, re
from repository.api import IRepository
from repository.component import implementations, Component, MetaComponent, abstract, implements


_repositories = {}


class MetaRepository(MetaComponent):
"""Metaclass of the Repository class, used mainly for ensuring
that repository id's are wellformed and unique"""

valid_id = re.compile("^[a-z][_a-z0-9]*$")

def __new__(cls, name, bases, d):
new_class = super(MetaRepository, cls).__new__(cls, name, bases, d)
if new_class in implementations(IRepository):
id = str(new_class.id())
if id in _repositories:
# Doctest test can duplicate a processor
# This can be identify by the conditon "module == '__main__'"
new_path = os.path.realpath(inspect.getfile(new_class))
id_path = os.path.realpath(inspect.getfile(_repositories[id]))
if new_class.__module__ == '__main__':
new_class = _repositories[id]
elif _repositories[id].__module__ == '__main__':
pass
elif new_path == id_path:
new_class = _repositories[id]
else:
raise ApiError("%s and %s have the same id: '%s'"
% (new_class.__name__,
_repositories[id].__name__, id))
if not MetaRepository.valid_id.match(id):
raise ApiError("%s has a malformed id: '%s'"
% (new_class.__name__, id))

_repositories[id] = new_class

return new_class


class Repository(Component, metaclass=MetaRepository):

url = None
vendor = None
vendor_client = None
vendor_instance = None
debug = False

abstract()
implements(IRepository)


def repositories(interface=IRepository, recurse=True):
"""Returns the objects implementing a given interface and, if recurse,
any of the descendants of this interface."""
return implementations(interface, recurse)


def list_repositories(interface=IRepository, prefix=""):
print(prefix + interface.__name__)
if len(prefix):
underline_char = '-'
else:
underline_char = '='
print(prefix + underline_char * len(interface.__name__))
subinterfaces = interface.__subclasses__()
procs = repositories(interface, False)
for p in procs:
print(prefix + " * %s :" % p.id())
print(prefix + " \t\t%s" % p.name())

67 changes: 0 additions & 67 deletions repository/repository.py

This file was deleted.

8 changes: 7 additions & 1 deletion repository/vendors/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
__all__ = ["gitlab"]

from .gitlab import GitlabRepository
from .github import GithubRepository


# from pkgutil import extend_path
# __path__ = extend_path(__path__, __name__)
Loading