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.
"""
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 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, operator.gt)
def __lt__(self, other):
return limit_versions(self, other, operator.lt)
def __ge__(self, other):
return limit_versions(self, other, operator.ge)
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, operator.ne)
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)