Source code for coalib.bearlib.languages.Language

from collections import OrderedDict
from itertools import chain
from inspect import isclass, getmembers
import operator
import re
from operator import itemgetter

from packaging.version import Version, InvalidVersion

from coalib.settings.Annotations import typechain

[docs]class UnknownLanguageError(AttributeError, KeyError): """ This exception occurs when an unknown language is requested. """
[docs]class LanguageUberMeta(type): """ This class is used to hide the `all` attribute from the Language class. """ all = []
convert_int_float_str = typechain(int, float, str)
[docs]def parse_lang_str(string): """ Parses any given language `string` into name and a list of either ``int``, ``float``, or ``str`` versions (ignores leading whitespace): >>> parse_lang_str("Python") ('Python', []) >>> parse_lang_str("Python 3.3") ('Python', [3.3]) >>> parse_lang_str("Python 3.6, 3.3.1") ('Python', [3.6, '3.3.1']) >>> parse_lang_str("Objective C 3.6, 3") ('Objective C', [3.6, 3]) >>> parse_lang_str("Cobol, stupid!") Traceback (most recent call last): ... packaging.version.InvalidVersion: Invalid version: 'stupid!' >>> parse_lang_str("Cobol seems at least stupid ;)") ('Cobol seems at least stupid ;)', []) """ name, *str_versions = re.split(r'\s*,\s*', str(string).strip()) versions = [] for version in str_versions: version = convert_int_float_str(version) Version(str(version)) # raises if not valid versions.append(version) try: realname, version = name.rsplit(maxsplit=1) version = convert_int_float_str(version) Version(str(version)) except (ValueError, InvalidVersion): pass else: versions.insert(0, version) return realname, versions return name, versions
[docs]class LanguageMeta(type, metaclass=LanguageUberMeta): """ Metaclass for :class:`coalib.bearlib.languages.Language.Language`. Allows it being used as a decorator as well as implements the :meth:`.__contains__` operation and stores all languages created with the decorator. Ensures that ``.versions`` defined in language classes will be turned into sorted tuples of ``packaging.version.Version`` instances. The operators are defined on the class as well, so you can do the following: >>> @Language ... class SomeLang: ... versions = 2.7, 3.3, 3.4, 3.5, 3.6 >>> Language.SomeLang > 3.4 SomeLang 3.5, 3.6 >>> Language.SomeLang < 3.4 SomeLang 2.7, 3.3 >>> Language.SomeLang >= 3.4 SomeLang 3.4, 3.5, 3.6 >>> Language.SomeLang <= 3.4 SomeLang 2.7, 3.3, 3.4 >>> Language.SomeLang == 3.4 SomeLang 3.4 >>> Language.SomeLang != 3.4 SomeLang 2.7, 3.3, 3.5, 3.6 >>> Language.SomeLang == 1.0 Traceback (most recent call last): ... ValueError: No versions left """ def __new__(mcs, clsname, bases, clsattrs): for base in bases: if issubclass(base, Language) and base is not Language: for name, obj in base._attributes.items(): clsattrs.setdefault(name, obj) return type.__new__(mcs, clsname, bases, clsattrs) def __init__(cls, clsname, bases, clsattrs): cls.versions = tuple(sorted( Version(str(v)) for v in getattr(cls, 'versions', ()))) super().__init__(clsname, bases, clsattrs) def __hash__(cls): """ >>> isinstance(hash(Language), int) True """ return type.__hash__(cls) def __dir__(cls): return super().__dir__() + [lang.__name__ for lang in type(cls).all] def __getattr__(cls, item): try: return next(lang for lang in type(cls).all if item in lang) except StopIteration: raise UnknownLanguageError( 'Language `{}` is not a valid language name or ' 'not recognized by coala.'.format(item)) def __getitem__(cls, item): if isinstance(item, cls): return item if isclass(item) and issubclass(item, cls): return item() name, versions = parse_lang_str(item) language = getattr(cls, name) if not versions: return language() return language(*set(chain(*((language == v).versions for v in versions)))) def __call__(cls, *args): if cls is Language: assert len(args) == 1 arg = args[0] assert isclass(arg), \ 'This decorator is made for classes. Did you mean to use ' \ '`Language[%s]`?' % (repr(arg[0]),) class SubLanguageMeta(type(cls)): # Override __getattr__ of the LanguageMeta to get a dict with # the attributes def __getattr__(self, item): try: return OrderedDict( (version, self._attributes[item]) for version in self.versions) except KeyError: raise AttributeError forbidden_attributes = list( chain(map(itemgetter(0), getmembers(Language)), ('versions', 'aliases'))) class Sub(cls, metaclass=SubLanguageMeta): __qualname__ = arg.__qualname__ versions = tuple(sorted(getattr(arg, 'versions', ()))) aliases = tuple(sorted(getattr(arg, 'aliases', ()))) _attributes = {name: member for name, member in getmembers(arg) if not name.startswith('_') and name not in forbidden_attributes} Sub.__name__ = arg.__name__ type(cls).all.append(Sub) return Sub return super().__call__(*args) def __contains__(cls, item): name, versions = parse_lang_str(item) return str(name).lower() in map( str.lower, chain(cls.aliases, [cls.__qualname__, cls.__name__]) ) and (not versions or all(Version(str(version)) in cls.versions for version in versions)) def __gt__(cls, other): return cls is not Language and cls() > other def __lt__(cls, other): return cls is not Language and cls() < other def __ge__(cls, other): return cls is not Language and cls() >= other def __le__(cls, other): return cls is not Language and cls() <= other def __eq__(cls, other): return cls is not Language and cls() == other def __ne__(cls, other): return cls is not Language and cls() != other
[docs]class Language(metaclass=LanguageMeta): """ This class defines programming languages and their versions. You can define a new programming language as follows: >>> @Language ... class TrumpScript: ... __qualname__ = "America is great." ... aliases = 'ts', ... versions = 2.7, 3.3, 3.4, 3.5, 3.6 ... comment_delimiter = '#' ... string_delimiter = {"'": "'"} From a bear, you can simply parse the user given language string to get the instance of the Language you desire: >>> Language['trumpscript'] America is great. 2.7, 3.3, 3.4, 3.5, 3.6 >>> Language['ts 3.4, 3.6'] America is great. 3.4, 3.6 >>> Language['TS 3'] America is great. 3.3, 3.4, 3.5, 3.6 >>> Language['tS 1'] Traceback (most recent call last): ... ValueError: No versions left All given versions will be stored as a sorted tuple of ``packaging.version.Version`` instances: >>> Language.TrumpScript(3.4, 3.3).versions (<Version('3.3')>, <Version('3.4')>) The attributes are not accessible unless you have selected one - and only one - version of your language: >>> Language.TrumpScript(3.3, 3.4).comment_delimiter Traceback (most recent call last): ... AttributeError: You have to specify ONE version ... >>> Language.TrumpScript(3.3).comment_delimiter '#' If you don't know which version is the right one, just use this: >>> Language.TrumpScript().get_default_version() America is great. 3.6 To see which attributes are available, use the ``attributes`` property: >>> Language.TrumpScript(3.3).attributes ['comment_delimiter', 'string_delimiter'] You can access a dictionary of the attribute values for every version from the class: >>> Language.TrumpScript.comment_delimiter OrderedDict([(<Version('2.7')>, '#'), (<Version('3.3')>, '#'), \ (<Version('3.4')>, '#'), (<Version('3.5')>, '#'), (<Version('3.6')>, '#')]) Any nonexistent item will of course not be served: >>> Language.TrumpScript.unknown_delimiter Traceback (most recent call last): ... AttributeError **You now know the most important parts for writing a bear using languages. Read ahead if you want to know more about working with multiple versions of programming languages as well as derivative languages!** We can define derivative languages as follows: >>> @Language ... class TrumpScriptDerivative(Language.TrumpScript): ... __qualname__ = 'Shorter' ... comment_delimiter = '//' ... keywords = None >>> Language.TrumpScriptDerivative() Shorter 2.7, 3.3, 3.4, 3.5, 3.6 >>> Language.TrumpScriptDerivative().get_default_version().attributes ['comment_delimiter', 'keywords', 'string_delimiter'] >>> Language.TrumpScriptDerivative().get_default_version().keywords >>> Language.TrumpScriptDerivative().get_default_version().comment_delimiter '//' >>> Language.TrumpScriptDerivative().get_default_version().string_delimiter {"'": "'"} We can get an instance via this syntax as well: >>> Language[Language.TrumpScript] America is great. 2.7, 3.3, 3.4, 3.5, 3.6 >>> Language[Language.TrumpScript(3.6)] America is great. 3.6 As you see, you can use the `__qualname__` property. This will also affect the string representation and work as an implicit alias: >>> str(Language.TrumpScript(3.4)) 'America is great. 3.4' We can specify the version by instantiating the TrumpScript class now: >>> str(Language.TrumpScript(3.6)) 'America is great. 3.6' You can also define ranges of versions of languages: >>> (Language.TrumpScript > 3.3) <= 3.5 America is great. 3.4, 3.5 >>> Language.TrumpScript == 3 America is great. 3.3, 3.4, 3.5, 3.6 Those can be combined by the or operator: >>> (Language.TrumpScript == 3.6) | (Language.TrumpScript == 2) America is great. 2.7, 3.6 The `__contains__` operator of the class is defined as well for strings and instances. This is case insensitive and aliases are allowed: >>> Language.TrumpScript(3.6) in Language.TrumpScript True >>> 'ts 3.6, 3.5' in Language.TrumpScript True >>> 'TrumpScript 2.6' in Language.TrumpScript False >>> 'TrumpScript' in Language.TrumpScript True This also works on instances: >>> 'ts 3.6, 3.5' in (Language.TrumpScript == 3) True >>> 'ts 3.6,3.5' in ((Language.TrumpScript == 2) ... | Language.TrumpScript(3.5)) False >>> Language.TrumpScript(2.7, 3.5) in (Language.TrumpScript == 3) False >>> Language.TrumpScript(3.5) in (Language.TrumpScript == 3) True Any undefined language will obviously not be available: >>> Language.Cobol Traceback (most recent call last): ... UnknownLanguageError: No language found for `Cobol` """ def __init__(self, *versions): versions = [Version(str(v)) for v in versions] assert all(version in type(self).versions for version in versions) if not versions: self.versions = type(self).versions else: self.versions = tuple(sorted(versions)) def __getattr__(self, item): if len(self.versions) > 1: raise AttributeError('You have to specify ONE version of your ' 'language to retrieve attributes for it.') try: return self._attributes[item] except KeyError: if len(self.attributes) == 0: message = 'There are no available attributes for this language.' else: message = ('This is not a valid attribute! ' '\nThe following attributes are available:') message += '\n'.join(self.attributes) raise AttributeError(message) def __str__(self): result = type(self).__qualname__ if self.versions: result += ' ' + ', '.join(map(str, self.versions)) return result def __repr__(self): return str(self) def __gt__(self, other): return limit_versions(self, other, def __lt__(self, other): return limit_versions(self, other, def __ge__(self, other): return limit_versions(self, other, def __le__(self, other): return limit_versions(self, other, operator.le) def __eq__(self, other): return limit_versions(self, other, operator.eq) def __ne__(self, other): return limit_versions(self, other, def __or__(self, other): return type(self)(*chain(self.versions, other.versions)) def __contains__(self, item): item = Language[item] item_versions = set(item.versions) versions = set(self.versions) return (type(self) is type(item) and (item_versions.issubset(versions) or item_versions.issuperset(versions))) def __reduce__(self): return (Language.__getitem__, (str(self),)) @property def attributes(self): """ Retrieves the names of all attributes that are available for this language. """ return sorted(self._attributes.keys())
[docs] def get_default_version(self): """ Retrieves the latest version the user would want to choose from the given versions in self. (At a later point this might also retrieve a default version specifiable by the language definition, so keep using this!) """ return type(self)(self.versions[-1]) if self.versions else type(self)()
[docs]def limit_versions(language, limit, operator): """ Limits given languages with the given operator: :param language: A `Language` instance. :param limit: A number to limit the versions. :param operator: The operator to use for the limiting. :return: A new `Language` instance with limited versions. :raises ValueError: If no version is left anymore. """ if isinstance(limit, int): versions = [version for version in language.versions if operator(int(str(version).split('.')[0]), limit)] elif isinstance(limit, float): versions = [version for version in language.versions if operator(float('.'.join(str(version).split('.')[0:2])), limit)] else: versions = [version for version in language.versions if operator(version, Version(str(limit)))] if not versions: raise ValueError('No versions left') return type(language)(*versions)
[docs]class Languages(tuple): """ A ``tuple``-based container for :class:`coalib.bearlib.languages.Language` instances. It supports language identifiers in any format accepted by ``Language[...]``: >>> Languages(['C#', Language.Python == 3]) (C#, Python 3.3, 3.4, 3.5, 3.6) >>> Languages(['C#', Language.Python == '3.6']) (C#, Python 3.6) >>> Languages(['C#', 'Python 2.7']) (C#, Python 2.7) It provides :meth:`.__contains__` for checking if a given language identifier is included: >>> 'Python 2.7, 3.5' in Languages([Language.Python()]) True >>> 'Py 3.3' in Languages(['Python 2']) False >>> 'csharp' in Languages(['C#', Language.Python == 3.6]) True """ def __new__(cls, items): return tuple.__new__(cls, (Language[i] for i in items)) def __contains__(self, item): return any(item in lang for lang in self)