Fixed import error after last commit.

pull/1958/head v1.1.2-beta.8
morpheus65535 2 years ago
parent 131b4e5cde
commit a338de147e

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
from aniso8601.date import get_date_resolution, parse_date
from aniso8601.duration import get_duration_resolution, parse_duration
from aniso8601.interval import (
get_interval_resolution,
get_repeating_interval_resolution,
parse_interval,
parse_repeating_interval,
)
# Import the main parsing functions so they are readily available
from aniso8601.time import (
get_datetime_resolution,
get_time_resolution,
parse_datetime,
parse_time,
)
__version__ = "9.0.1"

@ -0,0 +1,614 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
import calendar
from collections import namedtuple
from aniso8601.exceptions import (
DayOutOfBoundsError,
HoursOutOfBoundsError,
ISOFormatError,
LeapSecondError,
MidnightBoundsError,
MinutesOutOfBoundsError,
MonthOutOfBoundsError,
SecondsOutOfBoundsError,
WeekOutOfBoundsError,
YearOutOfBoundsError,
)
DateTuple = namedtuple("Date", ["YYYY", "MM", "DD", "Www", "D", "DDD"])
TimeTuple = namedtuple("Time", ["hh", "mm", "ss", "tz"])
DatetimeTuple = namedtuple("Datetime", ["date", "time"])
DurationTuple = namedtuple(
"Duration", ["PnY", "PnM", "PnW", "PnD", "TnH", "TnM", "TnS"]
)
IntervalTuple = namedtuple("Interval", ["start", "end", "duration"])
RepeatingIntervalTuple = namedtuple("RepeatingInterval", ["R", "Rnn", "interval"])
TimezoneTuple = namedtuple("Timezone", ["negative", "Z", "hh", "mm", "name"])
Limit = namedtuple(
"Limit",
[
"casterrorstring",
"min",
"max",
"rangeexception",
"rangeerrorstring",
"rangefunc",
],
)
def cast(
value,
castfunction,
caughtexceptions=(ValueError,),
thrownexception=ISOFormatError,
thrownmessage=None,
):
try:
result = castfunction(value)
except caughtexceptions:
raise thrownexception(thrownmessage)
return result
def range_check(valuestr, limit):
# Returns cast value if in range, raises defined exceptions on failure
if valuestr is None:
return None
if "." in valuestr:
castfunc = float
else:
castfunc = int
value = cast(valuestr, castfunc, thrownmessage=limit.casterrorstring)
if limit.min is not None and value < limit.min:
raise limit.rangeexception(limit.rangeerrorstring)
if limit.max is not None and value > limit.max:
raise limit.rangeexception(limit.rangeerrorstring)
return value
class BaseTimeBuilder(object):
# Limit tuple format cast function, cast error string,
# lower limit, upper limit, limit error string
DATE_YYYY_LIMIT = Limit(
"Invalid year string.",
0000,
9999,
YearOutOfBoundsError,
"Year must be between 1..9999.",
range_check,
)
DATE_MM_LIMIT = Limit(
"Invalid month string.",
1,
12,
MonthOutOfBoundsError,
"Month must be between 1..12.",
range_check,
)
DATE_DD_LIMIT = Limit(
"Invalid day string.",
1,
31,
DayOutOfBoundsError,
"Day must be between 1..31.",
range_check,
)
DATE_WWW_LIMIT = Limit(
"Invalid week string.",
1,
53,
WeekOutOfBoundsError,
"Week number must be between 1..53.",
range_check,
)
DATE_D_LIMIT = Limit(
"Invalid weekday string.",
1,
7,
DayOutOfBoundsError,
"Weekday number must be between 1..7.",
range_check,
)
DATE_DDD_LIMIT = Limit(
"Invalid ordinal day string.",
1,
366,
DayOutOfBoundsError,
"Ordinal day must be between 1..366.",
range_check,
)
TIME_HH_LIMIT = Limit(
"Invalid hour string.",
0,
24,
HoursOutOfBoundsError,
"Hour must be between 0..24 with " "24 representing midnight.",
range_check,
)
TIME_MM_LIMIT = Limit(
"Invalid minute string.",
0,
59,
MinutesOutOfBoundsError,
"Minute must be between 0..59.",
range_check,
)
TIME_SS_LIMIT = Limit(
"Invalid second string.",
0,
60,
SecondsOutOfBoundsError,
"Second must be between 0..60 with " "60 representing a leap second.",
range_check,
)
TZ_HH_LIMIT = Limit(
"Invalid timezone hour string.",
0,
23,
HoursOutOfBoundsError,
"Hour must be between 0..23.",
range_check,
)
TZ_MM_LIMIT = Limit(
"Invalid timezone minute string.",
0,
59,
MinutesOutOfBoundsError,
"Minute must be between 0..59.",
range_check,
)
DURATION_PNY_LIMIT = Limit(
"Invalid year duration string.",
0,
None,
ISOFormatError,
"Duration years component must be positive.",
range_check,
)
DURATION_PNM_LIMIT = Limit(
"Invalid month duration string.",
0,
None,
ISOFormatError,
"Duration months component must be positive.",
range_check,
)
DURATION_PNW_LIMIT = Limit(
"Invalid week duration string.",
0,
None,
ISOFormatError,
"Duration weeks component must be positive.",
range_check,
)
DURATION_PND_LIMIT = Limit(
"Invalid day duration string.",
0,
None,
ISOFormatError,
"Duration days component must be positive.",
range_check,
)
DURATION_TNH_LIMIT = Limit(
"Invalid hour duration string.",
0,
None,
ISOFormatError,
"Duration hours component must be positive.",
range_check,
)
DURATION_TNM_LIMIT = Limit(
"Invalid minute duration string.",
0,
None,
ISOFormatError,
"Duration minutes component must be positive.",
range_check,
)
DURATION_TNS_LIMIT = Limit(
"Invalid second duration string.",
0,
None,
ISOFormatError,
"Duration seconds component must be positive.",
range_check,
)
INTERVAL_RNN_LIMIT = Limit(
"Invalid duration repetition string.",
0,
None,
ISOFormatError,
"Duration repetition count must be positive.",
range_check,
)
DATE_RANGE_DICT = {
"YYYY": DATE_YYYY_LIMIT,
"MM": DATE_MM_LIMIT,
"DD": DATE_DD_LIMIT,
"Www": DATE_WWW_LIMIT,
"D": DATE_D_LIMIT,
"DDD": DATE_DDD_LIMIT,
}
TIME_RANGE_DICT = {"hh": TIME_HH_LIMIT, "mm": TIME_MM_LIMIT, "ss": TIME_SS_LIMIT}
DURATION_RANGE_DICT = {
"PnY": DURATION_PNY_LIMIT,
"PnM": DURATION_PNM_LIMIT,
"PnW": DURATION_PNW_LIMIT,
"PnD": DURATION_PND_LIMIT,
"TnH": DURATION_TNH_LIMIT,
"TnM": DURATION_TNM_LIMIT,
"TnS": DURATION_TNS_LIMIT,
}
REPEATING_INTERVAL_RANGE_DICT = {"Rnn": INTERVAL_RNN_LIMIT}
TIMEZONE_RANGE_DICT = {"hh": TZ_HH_LIMIT, "mm": TZ_MM_LIMIT}
LEAP_SECONDS_SUPPORTED = False
@classmethod
def build_date(cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None):
raise NotImplementedError
@classmethod
def build_time(cls, hh=None, mm=None, ss=None, tz=None):
raise NotImplementedError
@classmethod
def build_datetime(cls, date, time):
raise NotImplementedError
@classmethod
def build_duration(
cls, PnY=None, PnM=None, PnW=None, PnD=None, TnH=None, TnM=None, TnS=None
):
raise NotImplementedError
@classmethod
def build_interval(cls, start=None, end=None, duration=None):
# start, end, and duration are all tuples
raise NotImplementedError
@classmethod
def build_repeating_interval(cls, R=None, Rnn=None, interval=None):
# interval is a tuple
raise NotImplementedError
@classmethod
def build_timezone(cls, negative=None, Z=None, hh=None, mm=None, name=""):
raise NotImplementedError
@classmethod
def range_check_date(
cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None, rangedict=None
):
if rangedict is None:
rangedict = cls.DATE_RANGE_DICT
if "YYYY" in rangedict:
YYYY = rangedict["YYYY"].rangefunc(YYYY, rangedict["YYYY"])
if "MM" in rangedict:
MM = rangedict["MM"].rangefunc(MM, rangedict["MM"])
if "DD" in rangedict:
DD = rangedict["DD"].rangefunc(DD, rangedict["DD"])
if "Www" in rangedict:
Www = rangedict["Www"].rangefunc(Www, rangedict["Www"])
if "D" in rangedict:
D = rangedict["D"].rangefunc(D, rangedict["D"])
if "DDD" in rangedict:
DDD = rangedict["DDD"].rangefunc(DDD, rangedict["DDD"])
if DD is not None:
# Check calendar
if DD > calendar.monthrange(YYYY, MM)[1]:
raise DayOutOfBoundsError(
"{0} is out of range for {1}-{2}".format(DD, YYYY, MM)
)
if DDD is not None:
if calendar.isleap(YYYY) is False and DDD == 366:
raise DayOutOfBoundsError(
"{0} is only valid for leap year.".format(DDD)
)
return (YYYY, MM, DD, Www, D, DDD)
@classmethod
def range_check_time(cls, hh=None, mm=None, ss=None, tz=None, rangedict=None):
# Used for midnight and leap second handling
midnight = False # Handle hh = '24' specially
if rangedict is None:
rangedict = cls.TIME_RANGE_DICT
if "hh" in rangedict:
try:
hh = rangedict["hh"].rangefunc(hh, rangedict["hh"])
except HoursOutOfBoundsError as e:
if float(hh) > 24 and float(hh) < 25:
raise MidnightBoundsError("Hour 24 may only represent midnight.")
raise e
if "mm" in rangedict:
mm = rangedict["mm"].rangefunc(mm, rangedict["mm"])
if "ss" in rangedict:
ss = rangedict["ss"].rangefunc(ss, rangedict["ss"])
if hh is not None and hh == 24:
midnight = True
# Handle midnight range
if midnight is True and (
(mm is not None and mm != 0) or (ss is not None and ss != 0)
):
raise MidnightBoundsError("Hour 24 may only represent midnight.")
if cls.LEAP_SECONDS_SUPPORTED is True:
if hh != 23 and mm != 59 and ss == 60:
raise cls.TIME_SS_LIMIT.rangeexception(
cls.TIME_SS_LIMIT.rangeerrorstring
)
else:
if hh == 23 and mm == 59 and ss == 60:
# https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
raise LeapSecondError("Leap seconds are not supported.")
if ss == 60:
raise cls.TIME_SS_LIMIT.rangeexception(
cls.TIME_SS_LIMIT.rangeerrorstring
)
return (hh, mm, ss, tz)
@classmethod
def range_check_duration(
cls,
PnY=None,
PnM=None,
PnW=None,
PnD=None,
TnH=None,
TnM=None,
TnS=None,
rangedict=None,
):
if rangedict is None:
rangedict = cls.DURATION_RANGE_DICT
if "PnY" in rangedict:
PnY = rangedict["PnY"].rangefunc(PnY, rangedict["PnY"])
if "PnM" in rangedict:
PnM = rangedict["PnM"].rangefunc(PnM, rangedict["PnM"])
if "PnW" in rangedict:
PnW = rangedict["PnW"].rangefunc(PnW, rangedict["PnW"])
if "PnD" in rangedict:
PnD = rangedict["PnD"].rangefunc(PnD, rangedict["PnD"])
if "TnH" in rangedict:
TnH = rangedict["TnH"].rangefunc(TnH, rangedict["TnH"])
if "TnM" in rangedict:
TnM = rangedict["TnM"].rangefunc(TnM, rangedict["TnM"])
if "TnS" in rangedict:
TnS = rangedict["TnS"].rangefunc(TnS, rangedict["TnS"])
return (PnY, PnM, PnW, PnD, TnH, TnM, TnS)
@classmethod
def range_check_repeating_interval(
cls, R=None, Rnn=None, interval=None, rangedict=None
):
if rangedict is None:
rangedict = cls.REPEATING_INTERVAL_RANGE_DICT
if "Rnn" in rangedict:
Rnn = rangedict["Rnn"].rangefunc(Rnn, rangedict["Rnn"])
return (R, Rnn, interval)
@classmethod
def range_check_timezone(
cls, negative=None, Z=None, hh=None, mm=None, name="", rangedict=None
):
if rangedict is None:
rangedict = cls.TIMEZONE_RANGE_DICT
if "hh" in rangedict:
hh = rangedict["hh"].rangefunc(hh, rangedict["hh"])
if "mm" in rangedict:
mm = rangedict["mm"].rangefunc(mm, rangedict["mm"])
return (negative, Z, hh, mm, name)
@classmethod
def _build_object(cls, parsetuple):
# Given a TupleBuilder tuple, build the correct object
if type(parsetuple) is DateTuple:
return cls.build_date(
YYYY=parsetuple.YYYY,
MM=parsetuple.MM,
DD=parsetuple.DD,
Www=parsetuple.Www,
D=parsetuple.D,
DDD=parsetuple.DDD,
)
if type(parsetuple) is TimeTuple:
return cls.build_time(
hh=parsetuple.hh, mm=parsetuple.mm, ss=parsetuple.ss, tz=parsetuple.tz
)
if type(parsetuple) is DatetimeTuple:
return cls.build_datetime(parsetuple.date, parsetuple.time)
if type(parsetuple) is DurationTuple:
return cls.build_duration(
PnY=parsetuple.PnY,
PnM=parsetuple.PnM,
PnW=parsetuple.PnW,
PnD=parsetuple.PnD,
TnH=parsetuple.TnH,
TnM=parsetuple.TnM,
TnS=parsetuple.TnS,
)
if type(parsetuple) is IntervalTuple:
return cls.build_interval(
start=parsetuple.start, end=parsetuple.end, duration=parsetuple.duration
)
if type(parsetuple) is RepeatingIntervalTuple:
return cls.build_repeating_interval(
R=parsetuple.R, Rnn=parsetuple.Rnn, interval=parsetuple.interval
)
return cls.build_timezone(
negative=parsetuple.negative,
Z=parsetuple.Z,
hh=parsetuple.hh,
mm=parsetuple.mm,
name=parsetuple.name,
)
@classmethod
def _is_interval_end_concise(cls, endtuple):
if type(endtuple) is TimeTuple:
return True
if type(endtuple) is DatetimeTuple:
enddatetuple = endtuple.date
else:
enddatetuple = endtuple
if enddatetuple.YYYY is None:
return True
return False
@classmethod
def _combine_concise_interval_tuples(cls, starttuple, conciseendtuple):
starttimetuple = None
startdatetuple = None
endtimetuple = None
enddatetuple = None
if type(starttuple) is DateTuple:
startdatetuple = starttuple
else:
# Start is a datetime
starttimetuple = starttuple.time
startdatetuple = starttuple.date
if type(conciseendtuple) is DateTuple:
enddatetuple = conciseendtuple
elif type(conciseendtuple) is DatetimeTuple:
enddatetuple = conciseendtuple.date
endtimetuple = conciseendtuple.time
else:
# Time
endtimetuple = conciseendtuple
if enddatetuple is not None:
if enddatetuple.YYYY is None and enddatetuple.MM is None:
newenddatetuple = DateTuple(
YYYY=startdatetuple.YYYY,
MM=startdatetuple.MM,
DD=enddatetuple.DD,
Www=enddatetuple.Www,
D=enddatetuple.D,
DDD=enddatetuple.DDD,
)
else:
newenddatetuple = DateTuple(
YYYY=startdatetuple.YYYY,
MM=enddatetuple.MM,
DD=enddatetuple.DD,
Www=enddatetuple.Www,
D=enddatetuple.D,
DDD=enddatetuple.DDD,
)
if (starttimetuple is not None and starttimetuple.tz is not None) and (
endtimetuple is not None and endtimetuple.tz != starttimetuple.tz
):
# Copy the timezone across
endtimetuple = TimeTuple(
hh=endtimetuple.hh,
mm=endtimetuple.mm,
ss=endtimetuple.ss,
tz=starttimetuple.tz,
)
if enddatetuple is not None and endtimetuple is None:
return newenddatetuple
if enddatetuple is not None and endtimetuple is not None:
return TupleBuilder.build_datetime(newenddatetuple, endtimetuple)
return TupleBuilder.build_datetime(startdatetuple, endtimetuple)
class TupleBuilder(BaseTimeBuilder):
# Builder used to return the arguments as a tuple, cleans up some parse methods
@classmethod
def build_date(cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None):
return DateTuple(YYYY, MM, DD, Www, D, DDD)
@classmethod
def build_time(cls, hh=None, mm=None, ss=None, tz=None):
return TimeTuple(hh, mm, ss, tz)
@classmethod
def build_datetime(cls, date, time):
return DatetimeTuple(date, time)
@classmethod
def build_duration(
cls, PnY=None, PnM=None, PnW=None, PnD=None, TnH=None, TnM=None, TnS=None
):
return DurationTuple(PnY, PnM, PnW, PnD, TnH, TnM, TnS)
@classmethod
def build_interval(cls, start=None, end=None, duration=None):
return IntervalTuple(start, end, duration)
@classmethod
def build_repeating_interval(cls, R=None, Rnn=None, interval=None):
return RepeatingIntervalTuple(R, Rnn, interval)
@classmethod
def build_timezone(cls, negative=None, Z=None, hh=None, mm=None, name=""):
return TimezoneTuple(negative, Z, hh, mm, name)

@ -0,0 +1,705 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
import datetime
from collections import namedtuple
from functools import partial
from aniso8601.builders import (
BaseTimeBuilder,
DatetimeTuple,
DateTuple,
Limit,
TimeTuple,
TupleBuilder,
cast,
range_check,
)
from aniso8601.exceptions import (
DayOutOfBoundsError,
HoursOutOfBoundsError,
ISOFormatError,
LeapSecondError,
MidnightBoundsError,
MinutesOutOfBoundsError,
MonthOutOfBoundsError,
SecondsOutOfBoundsError,
WeekOutOfBoundsError,
YearOutOfBoundsError,
)
from aniso8601.utcoffset import UTCOffset
DAYS_PER_YEAR = 365
DAYS_PER_MONTH = 30
DAYS_PER_WEEK = 7
HOURS_PER_DAY = 24
MINUTES_PER_HOUR = 60
MINUTES_PER_DAY = MINUTES_PER_HOUR * HOURS_PER_DAY
SECONDS_PER_MINUTE = 60
SECONDS_PER_DAY = MINUTES_PER_DAY * SECONDS_PER_MINUTE
MICROSECONDS_PER_SECOND = int(1e6)
MICROSECONDS_PER_MINUTE = 60 * MICROSECONDS_PER_SECOND
MICROSECONDS_PER_HOUR = 60 * MICROSECONDS_PER_MINUTE
MICROSECONDS_PER_DAY = 24 * MICROSECONDS_PER_HOUR
MICROSECONDS_PER_WEEK = 7 * MICROSECONDS_PER_DAY
MICROSECONDS_PER_MONTH = DAYS_PER_MONTH * MICROSECONDS_PER_DAY
MICROSECONDS_PER_YEAR = DAYS_PER_YEAR * MICROSECONDS_PER_DAY
TIMEDELTA_MAX_DAYS = datetime.timedelta.max.days
FractionalComponent = namedtuple(
"FractionalComponent", ["principal", "microsecondremainder"]
)
def year_range_check(valuestr, limit):
YYYYstr = valuestr
# Truncated dates, like '19', refer to 1900-1999 inclusive,
# we simply parse to 1900
if len(valuestr) < 4:
# Shift 0s in from the left to form complete year
YYYYstr = valuestr.ljust(4, "0")
return range_check(YYYYstr, limit)
def fractional_range_check(conversion, valuestr, limit):
if valuestr is None:
return None
if "." in valuestr:
castfunc = partial(_cast_to_fractional_component, conversion)
else:
castfunc = int
value = cast(valuestr, castfunc, thrownmessage=limit.casterrorstring)
if type(value) is FractionalComponent:
tocheck = float(valuestr)
else:
tocheck = int(valuestr)
if limit.min is not None and tocheck < limit.min:
raise limit.rangeexception(limit.rangeerrorstring)
if limit.max is not None and tocheck > limit.max:
raise limit.rangeexception(limit.rangeerrorstring)
return value
def _cast_to_fractional_component(conversion, floatstr):
# Splits a string with a decimal point into an int, and
# int representing the floating point remainder as a number
# of microseconds, determined by multiplying by conversion
intpart, floatpart = floatstr.split(".")
intvalue = int(intpart)
preconvertedvalue = int(floatpart)
convertedvalue = (preconvertedvalue * conversion) // (10 ** len(floatpart))
return FractionalComponent(intvalue, convertedvalue)
class PythonTimeBuilder(BaseTimeBuilder):
# 0000 (1 BC) is not representable as a Python date
DATE_YYYY_LIMIT = Limit(
"Invalid year string.",
datetime.MINYEAR,
datetime.MAXYEAR,
YearOutOfBoundsError,
"Year must be between {0}..{1}.".format(datetime.MINYEAR, datetime.MAXYEAR),
year_range_check,
)
TIME_HH_LIMIT = Limit(
"Invalid hour string.",
0,
24,
HoursOutOfBoundsError,
"Hour must be between 0..24 with " "24 representing midnight.",
partial(fractional_range_check, MICROSECONDS_PER_HOUR),
)
TIME_MM_LIMIT = Limit(
"Invalid minute string.",
0,
59,
MinutesOutOfBoundsError,
"Minute must be between 0..59.",
partial(fractional_range_check, MICROSECONDS_PER_MINUTE),
)
TIME_SS_LIMIT = Limit(
"Invalid second string.",
0,
60,
SecondsOutOfBoundsError,
"Second must be between 0..60 with " "60 representing a leap second.",
partial(fractional_range_check, MICROSECONDS_PER_SECOND),
)
DURATION_PNY_LIMIT = Limit(
"Invalid year duration string.",
None,
None,
YearOutOfBoundsError,
None,
partial(fractional_range_check, MICROSECONDS_PER_YEAR),
)
DURATION_PNM_LIMIT = Limit(
"Invalid month duration string.",
None,
None,
MonthOutOfBoundsError,
None,
partial(fractional_range_check, MICROSECONDS_PER_MONTH),
)
DURATION_PNW_LIMIT = Limit(
"Invalid week duration string.",
None,
None,
WeekOutOfBoundsError,
None,
partial(fractional_range_check, MICROSECONDS_PER_WEEK),
)
DURATION_PND_LIMIT = Limit(
"Invalid day duration string.",
None,
None,
DayOutOfBoundsError,
None,
partial(fractional_range_check, MICROSECONDS_PER_DAY),
)
DURATION_TNH_LIMIT = Limit(
"Invalid hour duration string.",
None,
None,
HoursOutOfBoundsError,
None,
partial(fractional_range_check, MICROSECONDS_PER_HOUR),
)
DURATION_TNM_LIMIT = Limit(
"Invalid minute duration string.",
None,
None,
MinutesOutOfBoundsError,
None,
partial(fractional_range_check, MICROSECONDS_PER_MINUTE),
)
DURATION_TNS_LIMIT = Limit(
"Invalid second duration string.",
None,
None,
SecondsOutOfBoundsError,
None,
partial(fractional_range_check, MICROSECONDS_PER_SECOND),
)
DATE_RANGE_DICT = BaseTimeBuilder.DATE_RANGE_DICT
DATE_RANGE_DICT["YYYY"] = DATE_YYYY_LIMIT
TIME_RANGE_DICT = {"hh": TIME_HH_LIMIT, "mm": TIME_MM_LIMIT, "ss": TIME_SS_LIMIT}
DURATION_RANGE_DICT = {
"PnY": DURATION_PNY_LIMIT,
"PnM": DURATION_PNM_LIMIT,
"PnW": DURATION_PNW_LIMIT,
"PnD": DURATION_PND_LIMIT,
"TnH": DURATION_TNH_LIMIT,
"TnM": DURATION_TNM_LIMIT,
"TnS": DURATION_TNS_LIMIT,
}
@classmethod
def build_date(cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None):
YYYY, MM, DD, Www, D, DDD = cls.range_check_date(YYYY, MM, DD, Www, D, DDD)
if MM is None:
MM = 1
if DD is None:
DD = 1
if DDD is not None:
return PythonTimeBuilder._build_ordinal_date(YYYY, DDD)
if Www is not None:
return PythonTimeBuilder._build_week_date(YYYY, Www, isoday=D)
return datetime.date(YYYY, MM, DD)
@classmethod
def build_time(cls, hh=None, mm=None, ss=None, tz=None):
# Builds a time from the given parts, handling fractional arguments
# where necessary
hours = 0
minutes = 0
seconds = 0
microseconds = 0
hh, mm, ss, tz = cls.range_check_time(hh, mm, ss, tz)
if type(hh) is FractionalComponent:
hours = hh.principal
microseconds = hh.microsecondremainder
elif hh is not None:
hours = hh
if type(mm) is FractionalComponent:
minutes = mm.principal
microseconds = mm.microsecondremainder
elif mm is not None:
minutes = mm
if type(ss) is FractionalComponent:
seconds = ss.principal
microseconds = ss.microsecondremainder
elif ss is not None:
seconds = ss
(
hours,
minutes,
seconds,
microseconds,
) = PythonTimeBuilder._distribute_microseconds(
microseconds,
(hours, minutes, seconds),
(MICROSECONDS_PER_HOUR, MICROSECONDS_PER_MINUTE, MICROSECONDS_PER_SECOND),
)
# Move midnight into range
if hours == 24:
hours = 0
# Datetimes don't handle fractional components, so we use a timedelta
if tz is not None:
return (
datetime.datetime(
1, 1, 1, hour=hours, minute=minutes, tzinfo=cls._build_object(tz)
)
+ datetime.timedelta(seconds=seconds, microseconds=microseconds)
).timetz()
return (
datetime.datetime(1, 1, 1, hour=hours, minute=minutes)
+ datetime.timedelta(seconds=seconds, microseconds=microseconds)
).time()
@classmethod
def build_datetime(cls, date, time):
return datetime.datetime.combine(
cls._build_object(date), cls._build_object(time)
)
@classmethod
def build_duration(
cls, PnY=None, PnM=None, PnW=None, PnD=None, TnH=None, TnM=None, TnS=None
):
# PnY and PnM will be distributed to PnD, microsecond remainder to TnS
PnY, PnM, PnW, PnD, TnH, TnM, TnS = cls.range_check_duration(
PnY, PnM, PnW, PnD, TnH, TnM, TnS
)
seconds = TnS.principal
microseconds = TnS.microsecondremainder
return datetime.timedelta(
days=PnD,
seconds=seconds,
microseconds=microseconds,
minutes=TnM,
hours=TnH,
weeks=PnW,
)
@classmethod
def build_interval(cls, start=None, end=None, duration=None):
start, end, duration = cls.range_check_interval(start, end, duration)
if start is not None and end is not None:
# <start>/<end>
startobject = cls._build_object(start)
endobject = cls._build_object(end)
return (startobject, endobject)
durationobject = cls._build_object(duration)
# Determine if datetime promotion is required
datetimerequired = (
duration.TnH is not None
or duration.TnM is not None
or duration.TnS is not None
or durationobject.seconds != 0
or durationobject.microseconds != 0
)
if end is not None:
# <duration>/<end>
endobject = cls._build_object(end)
# Range check
if type(end) is DateTuple and datetimerequired is True:
# <end> is a date, and <duration> requires datetime resolution
return (
endobject,
cls.build_datetime(end, TupleBuilder.build_time()) - durationobject,
)
return (endobject, endobject - durationobject)
# <start>/<duration>
startobject = cls._build_object(start)
# Range check
if type(start) is DateTuple and datetimerequired is True:
# <start> is a date, and <duration> requires datetime resolution
return (
startobject,
cls.build_datetime(start, TupleBuilder.build_time()) + durationobject,
)
return (startobject, startobject + durationobject)
@classmethod
def build_repeating_interval(cls, R=None, Rnn=None, interval=None):
startobject = None
endobject = None
R, Rnn, interval = cls.range_check_repeating_interval(R, Rnn, interval)
if interval.start is not None:
startobject = cls._build_object(interval.start)
if interval.end is not None:
endobject = cls._build_object(interval.end)
if interval.duration is not None:
durationobject = cls._build_object(interval.duration)
else:
durationobject = endobject - startobject
if R is True:
if startobject is not None:
return cls._date_generator_unbounded(startobject, durationobject)
return cls._date_generator_unbounded(endobject, -durationobject)
iterations = int(Rnn)
if startobject is not None:
return cls._date_generator(startobject, durationobject, iterations)
return cls._date_generator(endobject, -durationobject, iterations)
@classmethod
def build_timezone(cls, negative=None, Z=None, hh=None, mm=None, name=""):
negative, Z, hh, mm, name = cls.range_check_timezone(negative, Z, hh, mm, name)
if Z is True:
# Z -> UTC
return UTCOffset(name="UTC", minutes=0)
tzhour = int(hh)
if mm is not None:
tzminute = int(mm)
else:
tzminute = 0
if negative is True:
return UTCOffset(name=name, minutes=-(tzhour * 60 + tzminute))
return UTCOffset(name=name, minutes=tzhour * 60 + tzminute)
@classmethod
def range_check_duration(
cls,
PnY=None,
PnM=None,
PnW=None,
PnD=None,
TnH=None,
TnM=None,
TnS=None,
rangedict=None,
):
years = 0
months = 0
days = 0
weeks = 0
hours = 0
minutes = 0
seconds = 0
microseconds = 0
PnY, PnM, PnW, PnD, TnH, TnM, TnS = BaseTimeBuilder.range_check_duration(
PnY, PnM, PnW, PnD, TnH, TnM, TnS, rangedict=cls.DURATION_RANGE_DICT
)
if PnY is not None:
if type(PnY) is FractionalComponent:
years = PnY.principal
microseconds = PnY.microsecondremainder
else:
years = PnY
if years * DAYS_PER_YEAR > TIMEDELTA_MAX_DAYS:
raise YearOutOfBoundsError("Duration exceeds maximum timedelta size.")
if PnM is not None:
if type(PnM) is FractionalComponent:
months = PnM.principal
microseconds = PnM.microsecondremainder
else:
months = PnM
if months * DAYS_PER_MONTH > TIMEDELTA_MAX_DAYS:
raise MonthOutOfBoundsError("Duration exceeds maximum timedelta size.")
if PnW is not None:
if type(PnW) is FractionalComponent:
weeks = PnW.principal
microseconds = PnW.microsecondremainder
else:
weeks = PnW
if weeks * DAYS_PER_WEEK > TIMEDELTA_MAX_DAYS:
raise WeekOutOfBoundsError("Duration exceeds maximum timedelta size.")
if PnD is not None:
if type(PnD) is FractionalComponent:
days = PnD.principal
microseconds = PnD.microsecondremainder
else:
days = PnD
if days > TIMEDELTA_MAX_DAYS:
raise DayOutOfBoundsError("Duration exceeds maximum timedelta size.")
if TnH is not None:
if type(TnH) is FractionalComponent:
hours = TnH.principal
microseconds = TnH.microsecondremainder
else:
hours = TnH
if hours // HOURS_PER_DAY > TIMEDELTA_MAX_DAYS:
raise HoursOutOfBoundsError("Duration exceeds maximum timedelta size.")
if TnM is not None:
if type(TnM) is FractionalComponent:
minutes = TnM.principal
microseconds = TnM.microsecondremainder
else:
minutes = TnM
if minutes // MINUTES_PER_DAY > TIMEDELTA_MAX_DAYS:
raise MinutesOutOfBoundsError(
"Duration exceeds maximum timedelta size."
)
if TnS is not None:
if type(TnS) is FractionalComponent:
seconds = TnS.principal
microseconds = TnS.microsecondremainder
else:
seconds = TnS
if seconds // SECONDS_PER_DAY > TIMEDELTA_MAX_DAYS:
raise SecondsOutOfBoundsError(
"Duration exceeds maximum timedelta size."
)
(
years,
months,
weeks,
days,
hours,
minutes,
seconds,
microseconds,
) = PythonTimeBuilder._distribute_microseconds(
microseconds,
(years, months, weeks, days, hours, minutes, seconds),
(
MICROSECONDS_PER_YEAR,
MICROSECONDS_PER_MONTH,
MICROSECONDS_PER_WEEK,
MICROSECONDS_PER_DAY,
MICROSECONDS_PER_HOUR,
MICROSECONDS_PER_MINUTE,
MICROSECONDS_PER_SECOND,
),
)
# Note that weeks can be handled without conversion to days
totaldays = years * DAYS_PER_YEAR + months * DAYS_PER_MONTH + days
# Check against timedelta limits
if (
totaldays
+ weeks * DAYS_PER_WEEK
+ hours // HOURS_PER_DAY
+ minutes // MINUTES_PER_DAY
+ seconds // SECONDS_PER_DAY
> TIMEDELTA_MAX_DAYS
):
raise DayOutOfBoundsError("Duration exceeds maximum timedelta size.")
return (
None,
None,
weeks,
totaldays,
hours,
minutes,
FractionalComponent(seconds, microseconds),
)
@classmethod
def range_check_interval(cls, start=None, end=None, duration=None):
# Handles concise format, range checks any potential durations
if start is not None and end is not None:
# <start>/<end>
# Handle concise format
if cls._is_interval_end_concise(end) is True:
end = cls._combine_concise_interval_tuples(start, end)
return (start, end, duration)
durationobject = cls._build_object(duration)
if end is not None:
# <duration>/<end>
endobject = cls._build_object(end)
# Range check
if type(end) is DateTuple:
enddatetime = cls.build_datetime(end, TupleBuilder.build_time())
if enddatetime - datetime.datetime.min < durationobject:
raise YearOutOfBoundsError("Interval end less than minimium date.")
else:
mindatetime = datetime.datetime.min
if end.time.tz is not None:
mindatetime = mindatetime.replace(tzinfo=endobject.tzinfo)
if endobject - mindatetime < durationobject:
raise YearOutOfBoundsError("Interval end less than minimium date.")
else:
# <start>/<duration>
startobject = cls._build_object(start)
# Range check
if type(start) is DateTuple:
startdatetime = cls.build_datetime(start, TupleBuilder.build_time())
if datetime.datetime.max - startdatetime < durationobject:
raise YearOutOfBoundsError(
"Interval end greater than maximum date."
)
else:
maxdatetime = datetime.datetime.max
if start.time.tz is not None:
maxdatetime = maxdatetime.replace(tzinfo=startobject.tzinfo)
if maxdatetime - startobject < durationobject:
raise YearOutOfBoundsError(
"Interval end greater than maximum date."
)
return (start, end, duration)
@staticmethod
def _build_week_date(isoyear, isoweek, isoday=None):
if isoday is None:
return PythonTimeBuilder._iso_year_start(isoyear) + datetime.timedelta(
weeks=isoweek - 1
)
return PythonTimeBuilder._iso_year_start(isoyear) + datetime.timedelta(
weeks=isoweek - 1, days=isoday - 1
)
@staticmethod
def _build_ordinal_date(isoyear, isoday):
# Day of year to a date
# https://stackoverflow.com/questions/2427555/python-question-year-and-day-of-year-to-date
builtdate = datetime.date(isoyear, 1, 1) + datetime.timedelta(days=isoday - 1)
return builtdate
@staticmethod
def _iso_year_start(isoyear):
# Given an ISO year, returns the equivalent of the start of the year
# on the Gregorian calendar (which is used by Python)
# Stolen from:
# http://stackoverflow.com/questions/304256/whats-the-best-way-to-find-the-inverse-of-datetime-isocalendar
# Determine the location of the 4th of January, the first week of
# the ISO year is the week containing the 4th of January
# http://en.wikipedia.org/wiki/ISO_week_date
fourth_jan = datetime.date(isoyear, 1, 4)
# Note the conversion from ISO day (1 - 7) and Python day (0 - 6)
delta = datetime.timedelta(days=fourth_jan.isoweekday() - 1)
# Return the start of the year
return fourth_jan - delta
@staticmethod
def _date_generator(startdate, timedelta, iterations):
currentdate = startdate
currentiteration = 0
while currentiteration < iterations:
yield currentdate
# Update the values
currentdate += timedelta
currentiteration += 1
@staticmethod
def _date_generator_unbounded(startdate, timedelta):
currentdate = startdate
while True:
yield currentdate
# Update the value
currentdate += timedelta
@staticmethod
def _distribute_microseconds(todistribute, recipients, reductions):
# Given a number of microseconds as int, a tuple of ints length n
# to distribute to, and a tuple of ints length n to divide todistribute
# by (from largest to smallest), returns a tuple of length n + 1, with
# todistribute divided across recipients using the reductions, with
# the final remainder returned as the final tuple member
results = []
remainder = todistribute
for index, reduction in enumerate(reductions):
additional, remainder = divmod(remainder, reduction)
results.append(recipients[index] + additional)
# Always return the remaining microseconds
results.append(remainder)
return tuple(results)

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.

@ -0,0 +1,838 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
import unittest
import aniso8601
from aniso8601.builders import (
BaseTimeBuilder,
DatetimeTuple,
DateTuple,
DurationTuple,
IntervalTuple,
RepeatingIntervalTuple,
TimeTuple,
TimezoneTuple,
TupleBuilder,
cast,
)
from aniso8601.exceptions import (
DayOutOfBoundsError,
HoursOutOfBoundsError,
ISOFormatError,
LeapSecondError,
MidnightBoundsError,
MinutesOutOfBoundsError,
MonthOutOfBoundsError,
SecondsOutOfBoundsError,
WeekOutOfBoundsError,
)
from aniso8601.tests.compat import mock
class LeapSecondSupportingTestBuilder(BaseTimeBuilder):
LEAP_SECONDS_SUPPORTED = True
class TestBuilderFunctions(unittest.TestCase):
def test_cast(self):
self.assertEqual(cast("1", int), 1)
self.assertEqual(cast("-2", int), -2)
self.assertEqual(cast("3", float), float(3))
self.assertEqual(cast("-4", float), float(-4))
self.assertEqual(cast("5.6", float), 5.6)
self.assertEqual(cast("-7.8", float), -7.8)
def test_cast_exception(self):
with self.assertRaises(ISOFormatError):
cast("asdf", int)
with self.assertRaises(ISOFormatError):
cast("asdf", float)
def test_cast_caughtexception(self):
def tester(value):
raise RuntimeError
with self.assertRaises(ISOFormatError):
cast("asdf", tester, caughtexceptions=(RuntimeError,))
def test_cast_thrownexception(self):
with self.assertRaises(RuntimeError):
cast("asdf", int, thrownexception=RuntimeError)
class TestBaseTimeBuilder(unittest.TestCase):
def test_build_date(self):
with self.assertRaises(NotImplementedError):
BaseTimeBuilder.build_date()
def test_build_time(self):
with self.assertRaises(NotImplementedError):
BaseTimeBuilder.build_time()
def test_build_datetime(self):
with self.assertRaises(NotImplementedError):
BaseTimeBuilder.build_datetime(None, None)
def test_build_duration(self):
with self.assertRaises(NotImplementedError):
BaseTimeBuilder.build_duration()
def test_build_interval(self):
with self.assertRaises(NotImplementedError):
BaseTimeBuilder.build_interval()
def test_build_repeating_interval(self):
with self.assertRaises(NotImplementedError):
BaseTimeBuilder.build_repeating_interval()
def test_build_timezone(self):
with self.assertRaises(NotImplementedError):
BaseTimeBuilder.build_timezone()
def test_range_check_date(self):
# Check the calendar for day ranges
with self.assertRaises(DayOutOfBoundsError):
BaseTimeBuilder.range_check_date(YYYY="0007", MM="02", DD="30")
with self.assertRaises(DayOutOfBoundsError):
BaseTimeBuilder.range_check_date(YYYY="0007", DDD="366")
with self.assertRaises(MonthOutOfBoundsError):
BaseTimeBuilder.range_check_date(YYYY="4333", MM="30", DD="30")
# 0 isn't a valid week number
with self.assertRaises(WeekOutOfBoundsError):
BaseTimeBuilder.range_check_date(YYYY="2003", Www="00")
# Week must not be larger than 53
with self.assertRaises(WeekOutOfBoundsError):
BaseTimeBuilder.range_check_date(YYYY="2004", Www="54")
# 0 isn't a valid day number
with self.assertRaises(DayOutOfBoundsError):
BaseTimeBuilder.range_check_date(YYYY="2001", Www="02", D="0")
# Day must not be larger than 7
with self.assertRaises(DayOutOfBoundsError):
BaseTimeBuilder.range_check_date(YYYY="2001", Www="02", D="8")
with self.assertRaises(DayOutOfBoundsError):
BaseTimeBuilder.range_check_date(YYYY="1981", DDD="000")
# Day must be 365, or 366, not larger
with self.assertRaises(DayOutOfBoundsError):
BaseTimeBuilder.range_check_date(YYYY="1234", DDD="000")
with self.assertRaises(DayOutOfBoundsError):
BaseTimeBuilder.range_check_date(YYYY="1234", DDD="367")
# https://bitbucket.org/nielsenb/aniso8601/issues/14/parsing-ordinal-dates-should-only-allow
with self.assertRaises(DayOutOfBoundsError):
BaseTimeBuilder.range_check_date(YYYY="1981", DDD="366")
# Make sure Nones pass through unmodified
self.assertEqual(
BaseTimeBuilder.range_check_date(rangedict={}),
(None, None, None, None, None, None),
)
def test_range_check_time(self):
# Leap seconds not supported
# https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
# https://bitbucket.org/nielsenb/aniso8601/issues/13/parsing-of-leap-second-gives-wildly
with self.assertRaises(LeapSecondError):
BaseTimeBuilder.range_check_time(hh="23", mm="59", ss="60")
with self.assertRaises(SecondsOutOfBoundsError):
BaseTimeBuilder.range_check_time(hh="00", mm="00", ss="60")
with self.assertRaises(SecondsOutOfBoundsError):
BaseTimeBuilder.range_check_time(hh="00", mm="00", ss="61")
with self.assertRaises(MinutesOutOfBoundsError):
BaseTimeBuilder.range_check_time(hh="00", mm="61")
with self.assertRaises(MinutesOutOfBoundsError):
BaseTimeBuilder.range_check_time(hh="00", mm="60")
with self.assertRaises(MinutesOutOfBoundsError):
BaseTimeBuilder.range_check_time(hh="00", mm="60.1")
with self.assertRaises(HoursOutOfBoundsError):
BaseTimeBuilder.range_check_time(hh="25")
# Hour 24 can only represent midnight
with self.assertRaises(MidnightBoundsError):
BaseTimeBuilder.range_check_time(hh="24", mm="00", ss="01")
with self.assertRaises(MidnightBoundsError):
BaseTimeBuilder.range_check_time(hh="24", mm="00.1")
with self.assertRaises(MidnightBoundsError):
BaseTimeBuilder.range_check_time(hh="24", mm="01")
with self.assertRaises(MidnightBoundsError):
BaseTimeBuilder.range_check_time(hh="24.1")
# Leap seconds not supported
# https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
# https://bitbucket.org/nielsenb/aniso8601/issues/13/parsing-of-leap-second-gives-wildly
with self.assertRaises(LeapSecondError):
BaseTimeBuilder.range_check_time(hh="23", mm="59", ss="60")
# Make sure Nones pass through unmodified
self.assertEqual(
BaseTimeBuilder.range_check_time(rangedict={}), (None, None, None, None)
)
def test_range_check_time_leap_seconds_supported(self):
self.assertEqual(
LeapSecondSupportingTestBuilder.range_check_time(hh="23", mm="59", ss="60"),
(23, 59, 60, None),
)
with self.assertRaises(SecondsOutOfBoundsError):
LeapSecondSupportingTestBuilder.range_check_time(hh="01", mm="02", ss="60")
def test_range_check_duration(self):
self.assertEqual(
BaseTimeBuilder.range_check_duration(),
(None, None, None, None, None, None, None),
)
self.assertEqual(
BaseTimeBuilder.range_check_duration(rangedict={}),
(None, None, None, None, None, None, None),
)
def test_range_check_repeating_interval(self):
self.assertEqual(
BaseTimeBuilder.range_check_repeating_interval(), (None, None, None)
)
self.assertEqual(
BaseTimeBuilder.range_check_repeating_interval(rangedict={}),
(None, None, None),
)
def test_range_check_timezone(self):
self.assertEqual(
BaseTimeBuilder.range_check_timezone(), (None, None, None, None, "")
)
self.assertEqual(
BaseTimeBuilder.range_check_timezone(rangedict={}),
(None, None, None, None, ""),
)
def test_build_object(self):
datetest = (
DateTuple("1", "2", "3", "4", "5", "6"),
{"YYYY": "1", "MM": "2", "DD": "3", "Www": "4", "D": "5", "DDD": "6"},
)
timetest = (
TimeTuple("1", "2", "3", TimezoneTuple(False, False, "4", "5", "tz name")),
{
"hh": "1",
"mm": "2",
"ss": "3",
"tz": TimezoneTuple(False, False, "4", "5", "tz name"),
},
)
datetimetest = (
DatetimeTuple(
DateTuple("1", "2", "3", "4", "5", "6"),
TimeTuple(
"7", "8", "9", TimezoneTuple(True, False, "10", "11", "tz name")
),
),
(
DateTuple("1", "2", "3", "4", "5", "6"),
TimeTuple(
"7", "8", "9", TimezoneTuple(True, False, "10", "11", "tz name")
),
),
)
durationtest = (
DurationTuple("1", "2", "3", "4", "5", "6", "7"),
{
"PnY": "1",
"PnM": "2",
"PnW": "3",
"PnD": "4",
"TnH": "5",
"TnM": "6",
"TnS": "7",
},
)
intervaltests = (
(
IntervalTuple(
DateTuple("1", "2", "3", "4", "5", "6"),
DateTuple("7", "8", "9", "10", "11", "12"),
None,
),
{
"start": DateTuple("1", "2", "3", "4", "5", "6"),
"end": DateTuple("7", "8", "9", "10", "11", "12"),
"duration": None,
},
),
(
IntervalTuple(
DateTuple("1", "2", "3", "4", "5", "6"),
None,
DurationTuple("7", "8", "9", "10", "11", "12", "13"),
),
{
"start": DateTuple("1", "2", "3", "4", "5", "6"),
"end": None,
"duration": DurationTuple("7", "8", "9", "10", "11", "12", "13"),
},
),
(
IntervalTuple(
None,
TimeTuple(
"1", "2", "3", TimezoneTuple(True, False, "4", "5", "tz name")
),
DurationTuple("6", "7", "8", "9", "10", "11", "12"),
),
{
"start": None,
"end": TimeTuple(
"1", "2", "3", TimezoneTuple(True, False, "4", "5", "tz name")
),
"duration": DurationTuple("6", "7", "8", "9", "10", "11", "12"),
},
),
)
repeatingintervaltests = (
(
RepeatingIntervalTuple(
True,
None,
IntervalTuple(
DateTuple("1", "2", "3", "4", "5", "6"),
DateTuple("7", "8", "9", "10", "11", "12"),
None,
),
),
{
"R": True,
"Rnn": None,
"interval": IntervalTuple(
DateTuple("1", "2", "3", "4", "5", "6"),
DateTuple("7", "8", "9", "10", "11", "12"),
None,
),
},
),
(
RepeatingIntervalTuple(
False,
"1",
IntervalTuple(
DatetimeTuple(
DateTuple("2", "3", "4", "5", "6", "7"),
TimeTuple("8", "9", "10", None),
),
DatetimeTuple(
DateTuple("11", "12", "13", "14", "15", "16"),
TimeTuple("17", "18", "19", None),
),
None,
),
),
{
"R": False,
"Rnn": "1",
"interval": IntervalTuple(
DatetimeTuple(
DateTuple("2", "3", "4", "5", "6", "7"),
TimeTuple("8", "9", "10", None),
),
DatetimeTuple(
DateTuple("11", "12", "13", "14", "15", "16"),
TimeTuple("17", "18", "19", None),
),
None,
),
},
),
)
timezonetest = (
TimezoneTuple(False, False, "1", "2", "+01:02"),
{"negative": False, "Z": False, "hh": "1", "mm": "2", "name": "+01:02"},
)
with mock.patch.object(
aniso8601.builders.BaseTimeBuilder, "build_date"
) as mock_build:
mock_build.return_value = datetest[0]
result = BaseTimeBuilder._build_object(datetest[0])
self.assertEqual(result, datetest[0])
mock_build.assert_called_once_with(**datetest[1])
with mock.patch.object(
aniso8601.builders.BaseTimeBuilder, "build_time"
) as mock_build:
mock_build.return_value = timetest[0]
result = BaseTimeBuilder._build_object(timetest[0])
self.assertEqual(result, timetest[0])
mock_build.assert_called_once_with(**timetest[1])
with mock.patch.object(
aniso8601.builders.BaseTimeBuilder, "build_datetime"
) as mock_build:
mock_build.return_value = datetimetest[0]
result = BaseTimeBuilder._build_object(datetimetest[0])
self.assertEqual(result, datetimetest[0])
mock_build.assert_called_once_with(*datetimetest[1])
with mock.patch.object(
aniso8601.builders.BaseTimeBuilder, "build_duration"
) as mock_build:
mock_build.return_value = durationtest[0]
result = BaseTimeBuilder._build_object(durationtest[0])
self.assertEqual(result, durationtest[0])
mock_build.assert_called_once_with(**durationtest[1])
for intervaltest in intervaltests:
with mock.patch.object(
aniso8601.builders.BaseTimeBuilder, "build_interval"
) as mock_build:
mock_build.return_value = intervaltest[0]
result = BaseTimeBuilder._build_object(intervaltest[0])
self.assertEqual(result, intervaltest[0])
mock_build.assert_called_once_with(**intervaltest[1])
for repeatingintervaltest in repeatingintervaltests:
with mock.patch.object(
aniso8601.builders.BaseTimeBuilder, "build_repeating_interval"
) as mock_build:
mock_build.return_value = repeatingintervaltest[0]
result = BaseTimeBuilder._build_object(repeatingintervaltest[0])
self.assertEqual(result, repeatingintervaltest[0])
mock_build.assert_called_once_with(**repeatingintervaltest[1])
with mock.patch.object(
aniso8601.builders.BaseTimeBuilder, "build_timezone"
) as mock_build:
mock_build.return_value = timezonetest[0]
result = BaseTimeBuilder._build_object(timezonetest[0])
self.assertEqual(result, timezonetest[0])
mock_build.assert_called_once_with(**timezonetest[1])
def test_is_interval_end_concise(self):
self.assertTrue(
BaseTimeBuilder._is_interval_end_concise(TimeTuple("1", "2", "3", None))
)
self.assertTrue(
BaseTimeBuilder._is_interval_end_concise(
DateTuple(None, "2", "3", "4", "5", "6")
)
)
self.assertTrue(
BaseTimeBuilder._is_interval_end_concise(
DatetimeTuple(
DateTuple(None, "2", "3", "4", "5", "6"),
TimeTuple("7", "8", "9", None),
)
)
)
self.assertFalse(
BaseTimeBuilder._is_interval_end_concise(
DateTuple("1", "2", "3", "4", "5", "6")
)
)
self.assertFalse(
BaseTimeBuilder._is_interval_end_concise(
DatetimeTuple(
DateTuple("1", "2", "3", "4", "5", "6"),
TimeTuple("7", "8", "9", None),
)
)
)
def test_combine_concise_interval_tuples(self):
testtuples = (
(
DateTuple("2020", "01", "01", None, None, None),
DateTuple(None, None, "02", None, None, None),
DateTuple("2020", "01", "02", None, None, None),
),
(
DateTuple("2008", "02", "15", None, None, None),
DateTuple(None, "03", "14", None, None, None),
DateTuple("2008", "03", "14", None, None, None),
),
(
DatetimeTuple(
DateTuple("2007", "12", "14", None, None, None),
TimeTuple("13", "30", None, None),
),
TimeTuple("15", "30", None, None),
DatetimeTuple(
DateTuple("2007", "12", "14", None, None, None),
TimeTuple("15", "30", None, None),
),
),
(
DatetimeTuple(
DateTuple("2007", "11", "13", None, None, None),
TimeTuple("09", "00", None, None),
),
DatetimeTuple(
DateTuple(None, None, "15", None, None, None),
TimeTuple("17", "00", None, None),
),
DatetimeTuple(
DateTuple("2007", "11", "15", None, None, None),
TimeTuple("17", "00", None, None),
),
),
(
DatetimeTuple(
DateTuple("2007", "11", "13", None, None, None),
TimeTuple("00", "00", None, None),
),
DatetimeTuple(
DateTuple(None, None, "16", None, None, None),
TimeTuple("00", "00", None, None),
),
DatetimeTuple(
DateTuple("2007", "11", "16", None, None, None),
TimeTuple("00", "00", None, None),
),
),
(
DatetimeTuple(
DateTuple("2007", "11", "13", None, None, None),
TimeTuple(
"09", "00", None, TimezoneTuple(False, True, None, None, "Z")
),
),
DatetimeTuple(
DateTuple(None, None, "15", None, None, None),
TimeTuple("17", "00", None, None),
),
DatetimeTuple(
DateTuple("2007", "11", "15", None, None, None),
TimeTuple(
"17", "00", None, TimezoneTuple(False, True, None, None, "Z")
),
),
),
)
for testtuple in testtuples:
result = BaseTimeBuilder._combine_concise_interval_tuples(
testtuple[0], testtuple[1]
)
self.assertEqual(result, testtuple[2])
class TestTupleBuilder(unittest.TestCase):
def test_build_date(self):
datetuple = TupleBuilder.build_date()
self.assertEqual(datetuple, DateTuple(None, None, None, None, None, None))
datetuple = TupleBuilder.build_date(
YYYY="1", MM="2", DD="3", Www="4", D="5", DDD="6"
)
self.assertEqual(datetuple, DateTuple("1", "2", "3", "4", "5", "6"))
def test_build_time(self):
testtuples = (
({}, TimeTuple(None, None, None, None)),
(
{"hh": "1", "mm": "2", "ss": "3", "tz": None},
TimeTuple("1", "2", "3", None),
),
(
{
"hh": "1",
"mm": "2",
"ss": "3",
"tz": TimezoneTuple(False, False, "4", "5", "tz name"),
},
TimeTuple(
"1", "2", "3", TimezoneTuple(False, False, "4", "5", "tz name")
),
),
)
for testtuple in testtuples:
self.assertEqual(TupleBuilder.build_time(**testtuple[0]), testtuple[1])
def test_build_datetime(self):
testtuples = (
(
{
"date": DateTuple("1", "2", "3", "4", "5", "6"),
"time": TimeTuple("7", "8", "9", None),
},
DatetimeTuple(
DateTuple("1", "2", "3", "4", "5", "6"),
TimeTuple("7", "8", "9", None),
),
),
(
{
"date": DateTuple("1", "2", "3", "4", "5", "6"),
"time": TimeTuple(
"7", "8", "9", TimezoneTuple(True, False, "10", "11", "tz name")
),
},
DatetimeTuple(
DateTuple("1", "2", "3", "4", "5", "6"),
TimeTuple(
"7", "8", "9", TimezoneTuple(True, False, "10", "11", "tz name")
),
),
),
)
for testtuple in testtuples:
self.assertEqual(TupleBuilder.build_datetime(**testtuple[0]), testtuple[1])
def test_build_duration(self):
testtuples = (
({}, DurationTuple(None, None, None, None, None, None, None)),
(
{
"PnY": "1",
"PnM": "2",
"PnW": "3",
"PnD": "4",
"TnH": "5",
"TnM": "6",
"TnS": "7",
},
DurationTuple("1", "2", "3", "4", "5", "6", "7"),
),
)
for testtuple in testtuples:
self.assertEqual(TupleBuilder.build_duration(**testtuple[0]), testtuple[1])
def test_build_interval(self):
testtuples = (
({}, IntervalTuple(None, None, None)),
(
{
"start": DateTuple("1", "2", "3", "4", "5", "6"),
"end": DateTuple("7", "8", "9", "10", "11", "12"),
},
IntervalTuple(
DateTuple("1", "2", "3", "4", "5", "6"),
DateTuple("7", "8", "9", "10", "11", "12"),
None,
),
),
(
{
"start": TimeTuple(
"1", "2", "3", TimezoneTuple(True, False, "7", "8", "tz name")
),
"end": TimeTuple(
"4", "5", "6", TimezoneTuple(False, False, "9", "10", "tz name")
),
},
IntervalTuple(
TimeTuple(
"1", "2", "3", TimezoneTuple(True, False, "7", "8", "tz name")
),
TimeTuple(
"4", "5", "6", TimezoneTuple(False, False, "9", "10", "tz name")
),
None,
),
),
(
{
"start": DatetimeTuple(
DateTuple("1", "2", "3", "4", "5", "6"),
TimeTuple(
"7",
"8",
"9",
TimezoneTuple(True, False, "10", "11", "tz name"),
),
),
"end": DatetimeTuple(
DateTuple("12", "13", "14", "15", "16", "17"),
TimeTuple(
"18",
"19",
"20",
TimezoneTuple(False, False, "21", "22", "tz name"),
),
),
},
IntervalTuple(
DatetimeTuple(
DateTuple("1", "2", "3", "4", "5", "6"),
TimeTuple(
"7",
"8",
"9",
TimezoneTuple(True, False, "10", "11", "tz name"),
),
),
DatetimeTuple(
DateTuple("12", "13", "14", "15", "16", "17"),
TimeTuple(
"18",
"19",
"20",
TimezoneTuple(False, False, "21", "22", "tz name"),
),
),
None,
),
),
(
{
"start": DateTuple("1", "2", "3", "4", "5", "6"),
"end": None,
"duration": DurationTuple("7", "8", "9", "10", "11", "12", "13"),
},
IntervalTuple(
DateTuple("1", "2", "3", "4", "5", "6"),
None,
DurationTuple("7", "8", "9", "10", "11", "12", "13"),
),
),
(
{
"start": None,
"end": TimeTuple(
"1", "2", "3", TimezoneTuple(True, False, "4", "5", "tz name")
),
"duration": DurationTuple("6", "7", "8", "9", "10", "11", "12"),
},
IntervalTuple(
None,
TimeTuple(
"1", "2", "3", TimezoneTuple(True, False, "4", "5", "tz name")
),
DurationTuple("6", "7", "8", "9", "10", "11", "12"),
),
),
)
for testtuple in testtuples:
self.assertEqual(TupleBuilder.build_interval(**testtuple[0]), testtuple[1])
def test_build_repeating_interval(self):
testtuples = (
({}, RepeatingIntervalTuple(None, None, None)),
(
{
"R": True,
"interval": IntervalTuple(
DateTuple("1", "2", "3", "4", "5", "6"),
DateTuple("7", "8", "9", "10", "11", "12"),
None,
),
},
RepeatingIntervalTuple(
True,
None,
IntervalTuple(
DateTuple("1", "2", "3", "4", "5", "6"),
DateTuple("7", "8", "9", "10", "11", "12"),
None,
),
),
),
(
{
"R": False,
"Rnn": "1",
"interval": IntervalTuple(
DatetimeTuple(
DateTuple("2", "3", "4", "5", "6", "7"),
TimeTuple("8", "9", "10", None),
),
DatetimeTuple(
DateTuple("11", "12", "13", "14", "15", "16"),
TimeTuple("17", "18", "19", None),
),
None,
),
},
RepeatingIntervalTuple(
False,
"1",
IntervalTuple(
DatetimeTuple(
DateTuple("2", "3", "4", "5", "6", "7"),
TimeTuple("8", "9", "10", None),
),
DatetimeTuple(
DateTuple("11", "12", "13", "14", "15", "16"),
TimeTuple("17", "18", "19", None),
),
None,
),
),
),
)
for testtuple in testtuples:
result = TupleBuilder.build_repeating_interval(**testtuple[0])
self.assertEqual(result, testtuple[1])
def test_build_timezone(self):
testtuples = (
({}, TimezoneTuple(None, None, None, None, "")),
(
{"negative": False, "Z": True, "name": "UTC"},
TimezoneTuple(False, True, None, None, "UTC"),
),
(
{"negative": False, "Z": False, "hh": "1", "mm": "2", "name": "+01:02"},
TimezoneTuple(False, False, "1", "2", "+01:02"),
),
(
{"negative": True, "Z": False, "hh": "1", "mm": "2", "name": "-01:02"},
TimezoneTuple(True, False, "1", "2", "-01:02"),
),
)
for testtuple in testtuples:
result = TupleBuilder.build_timezone(**testtuple[0])
self.assertEqual(result, testtuple[1])

File diff suppressed because it is too large Load Diff

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
import sys
PY2 = sys.version_info[0] == 2
if PY2: # pragma: no cover
range = xrange # pylint: disable=undefined-variable
else:
range = range
def is_string(tocheck):
# pylint: disable=undefined-variable
if PY2: # pragma: no cover
return isinstance(tocheck, str) or isinstance(tocheck, unicode)
return isinstance(tocheck, str)

@ -0,0 +1,161 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
from aniso8601.builders import TupleBuilder
from aniso8601.builders.python import PythonTimeBuilder
from aniso8601.compat import is_string
from aniso8601.exceptions import ISOFormatError
from aniso8601.resolution import DateResolution
def get_date_resolution(isodatestr):
# Valid string formats are:
#
# Y[YYY]
# YYYY-MM-DD
# YYYYMMDD
# YYYY-MM
# YYYY-Www
# YYYYWww
# YYYY-Www-D
# YYYYWwwD
# YYYY-DDD
# YYYYDDD
isodatetuple = parse_date(isodatestr, builder=TupleBuilder)
if isodatetuple.DDD is not None:
# YYYY-DDD
# YYYYDDD
return DateResolution.Ordinal
if isodatetuple.D is not None:
# YYYY-Www-D
# YYYYWwwD
return DateResolution.Weekday
if isodatetuple.Www is not None:
# YYYY-Www
# YYYYWww
return DateResolution.Week
if isodatetuple.DD is not None:
# YYYY-MM-DD
# YYYYMMDD
return DateResolution.Day
if isodatetuple.MM is not None:
# YYYY-MM
return DateResolution.Month
# Y[YYY]
return DateResolution.Year
def parse_date(isodatestr, builder=PythonTimeBuilder):
# Given a string in any ISO 8601 date format, return a datetime.date
# object that corresponds to the given date. Valid string formats are:
#
# Y[YYY]
# YYYY-MM-DD
# YYYYMMDD
# YYYY-MM
# YYYY-Www
# YYYYWww
# YYYY-Www-D
# YYYYWwwD
# YYYY-DDD
# YYYYDDD
if is_string(isodatestr) is False:
raise ValueError("Date must be string.")
if isodatestr.startswith("+") or isodatestr.startswith("-"):
raise NotImplementedError(
"ISO 8601 extended year representation " "not supported."
)
if len(isodatestr) == 0 or isodatestr.count("-") > 2:
raise ISOFormatError('"{0}" is not a valid ISO 8601 date.'.format(isodatestr))
yearstr = None
monthstr = None
daystr = None
weekstr = None
weekdaystr = None
ordinaldaystr = None
if len(isodatestr) <= 4:
# Y[YYY]
yearstr = isodatestr
elif "W" in isodatestr:
if len(isodatestr) == 10:
# YYYY-Www-D
yearstr = isodatestr[0:4]
weekstr = isodatestr[6:8]
weekdaystr = isodatestr[9]
elif len(isodatestr) == 8:
if "-" in isodatestr:
# YYYY-Www
yearstr = isodatestr[0:4]
weekstr = isodatestr[6:]
else:
# YYYYWwwD
yearstr = isodatestr[0:4]
weekstr = isodatestr[5:7]
weekdaystr = isodatestr[7]
elif len(isodatestr) == 7:
# YYYYWww
yearstr = isodatestr[0:4]
weekstr = isodatestr[5:]
elif len(isodatestr) == 7:
if "-" in isodatestr:
# YYYY-MM
yearstr = isodatestr[0:4]
monthstr = isodatestr[5:]
else:
# YYYYDDD
yearstr = isodatestr[0:4]
ordinaldaystr = isodatestr[4:]
elif len(isodatestr) == 8:
if "-" in isodatestr:
# YYYY-DDD
yearstr = isodatestr[0:4]
ordinaldaystr = isodatestr[5:]
else:
# YYYYMMDD
yearstr = isodatestr[0:4]
monthstr = isodatestr[4:6]
daystr = isodatestr[6:]
elif len(isodatestr) == 10:
# YYYY-MM-DD
yearstr = isodatestr[0:4]
monthstr = isodatestr[5:7]
daystr = isodatestr[8:]
else:
raise ISOFormatError('"{0}" is not a valid ISO 8601 date.'.format(isodatestr))
hascomponent = False
for componentstr in [yearstr, monthstr, daystr, weekstr, weekdaystr, ordinaldaystr]:
if componentstr is not None:
hascomponent = True
if componentstr.isdigit() is False:
raise ISOFormatError(
'"{0}" is not a valid ISO 8601 date.'.format(isodatestr)
)
if hascomponent is False:
raise ISOFormatError('"{0}" is not a valid ISO 8601 date.'.format(isodatestr))
return builder.build_date(
YYYY=yearstr,
MM=monthstr,
DD=daystr,
Www=weekstr,
D=weekdaystr,
DDD=ordinaldaystr,
)

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
def normalize(value):
"""Returns the string with decimal separators normalized."""
return value.replace(",", ".")

@ -0,0 +1,291 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
from aniso8601 import compat
from aniso8601.builders import TupleBuilder
from aniso8601.builders.python import PythonTimeBuilder
from aniso8601.date import parse_date
from aniso8601.decimalfraction import normalize
from aniso8601.exceptions import ISOFormatError
from aniso8601.resolution import DurationResolution
from aniso8601.time import parse_time
def get_duration_resolution(isodurationstr):
# Valid string formats are:
#
# PnYnMnDTnHnMnS (or any reduced precision equivalent)
# PnW
# P<date>T<time>
isodurationtuple = parse_duration(isodurationstr, builder=TupleBuilder)
if isodurationtuple.TnS is not None:
return DurationResolution.Seconds
if isodurationtuple.TnM is not None:
return DurationResolution.Minutes
if isodurationtuple.TnH is not None:
return DurationResolution.Hours
if isodurationtuple.PnD is not None:
return DurationResolution.Days
if isodurationtuple.PnW is not None:
return DurationResolution.Weeks
if isodurationtuple.PnM is not None:
return DurationResolution.Months
return DurationResolution.Years
def parse_duration(isodurationstr, builder=PythonTimeBuilder):
# Given a string representing an ISO 8601 duration, return a
# a duration built by the given builder. Valid formats are:
#
# PnYnMnDTnHnMnS (or any reduced precision equivalent)
# PnW
# P<date>T<time>
if compat.is_string(isodurationstr) is False:
raise ValueError("Duration must be string.")
if len(isodurationstr) == 0:
raise ISOFormatError(
'"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr)
)
if isodurationstr[0] != "P":
raise ISOFormatError("ISO 8601 duration must start with a P.")
# If Y, M, D, H, S, or W are in the string,
# assume it is a specified duration
if _has_any_component(isodurationstr, ["Y", "M", "D", "H", "S", "W"]) is True:
parseresult = _parse_duration_prescribed(isodurationstr)
return builder.build_duration(**parseresult)
if isodurationstr.find("T") != -1:
parseresult = _parse_duration_combined(isodurationstr)
return builder.build_duration(**parseresult)
raise ISOFormatError(
'"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr)
)
def _parse_duration_prescribed(isodurationstr):
# durationstr can be of the form PnYnMnDTnHnMnS or PnW
# Make sure the end character is valid
# https://bitbucket.org/nielsenb/aniso8601/issues/9/durations-with-trailing-garbage-are-parsed
if isodurationstr[-1] not in ["Y", "M", "D", "H", "S", "W"]:
raise ISOFormatError("ISO 8601 duration must end with a valid " "character.")
# Make sure only the lowest order element has decimal precision
durationstr = normalize(isodurationstr)
if durationstr.count(".") > 1:
raise ISOFormatError(
"ISO 8601 allows only lowest order element to " "have a decimal fraction."
)
seperatoridx = durationstr.find(".")
if seperatoridx != -1:
remaining = durationstr[seperatoridx + 1 : -1]
# There should only ever be 1 letter after a decimal if there is more
# then one, the string is invalid
if remaining.isdigit() is False:
raise ISOFormatError(
"ISO 8601 duration must end with " "a single valid character."
)
# Do not allow W in combination with other designators
# https://bitbucket.org/nielsenb/aniso8601/issues/2/week-designators-should-not-be-combinable
if (
durationstr.find("W") != -1
and _has_any_component(durationstr, ["Y", "M", "D", "H", "S"]) is True
):
raise ISOFormatError(
"ISO 8601 week designators may not be combined "
"with other time designators."
)
# Parse the elements of the duration
if durationstr.find("T") == -1:
return _parse_duration_prescribed_notime(durationstr)
return _parse_duration_prescribed_time(durationstr)
def _parse_duration_prescribed_notime(isodurationstr):
# durationstr can be of the form PnYnMnD or PnW
durationstr = normalize(isodurationstr)
yearstr = None
monthstr = None
daystr = None
weekstr = None
weekidx = durationstr.find("W")
yearidx = durationstr.find("Y")
monthidx = durationstr.find("M")
dayidx = durationstr.find("D")
if weekidx != -1:
weekstr = durationstr[1:-1]
elif yearidx != -1 and monthidx != -1 and dayidx != -1:
yearstr = durationstr[1:yearidx]
monthstr = durationstr[yearidx + 1 : monthidx]
daystr = durationstr[monthidx + 1 : -1]
elif yearidx != -1 and monthidx != -1:
yearstr = durationstr[1:yearidx]
monthstr = durationstr[yearidx + 1 : monthidx]
elif yearidx != -1 and dayidx != -1:
yearstr = durationstr[1:yearidx]
daystr = durationstr[yearidx + 1 : dayidx]
elif monthidx != -1 and dayidx != -1:
monthstr = durationstr[1:monthidx]
daystr = durationstr[monthidx + 1 : -1]
elif yearidx != -1:
yearstr = durationstr[1:-1]
elif monthidx != -1:
monthstr = durationstr[1:-1]
elif dayidx != -1:
daystr = durationstr[1:-1]
else:
raise ISOFormatError(
'"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr)
)
for componentstr in [yearstr, monthstr, daystr, weekstr]:
if componentstr is not None:
if "." in componentstr:
intstr, fractionalstr = componentstr.split(".", 1)
if intstr.isdigit() is False:
raise ISOFormatError(
'"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr)
)
else:
if componentstr.isdigit() is False:
raise ISOFormatError(
'"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr)
)
return {"PnY": yearstr, "PnM": monthstr, "PnW": weekstr, "PnD": daystr}
def _parse_duration_prescribed_time(isodurationstr):
# durationstr can be of the form PnYnMnDTnHnMnS
timeidx = isodurationstr.find("T")
datestr = isodurationstr[:timeidx]
timestr = normalize(isodurationstr[timeidx + 1 :])
hourstr = None
minutestr = None
secondstr = None
houridx = timestr.find("H")
minuteidx = timestr.find("M")
secondidx = timestr.find("S")
if houridx != -1 and minuteidx != -1 and secondidx != -1:
hourstr = timestr[0:houridx]
minutestr = timestr[houridx + 1 : minuteidx]
secondstr = timestr[minuteidx + 1 : -1]
elif houridx != -1 and minuteidx != -1:
hourstr = timestr[0:houridx]
minutestr = timestr[houridx + 1 : minuteidx]
elif houridx != -1 and secondidx != -1:
hourstr = timestr[0:houridx]
secondstr = timestr[houridx + 1 : -1]
elif minuteidx != -1 and secondidx != -1:
minutestr = timestr[0:minuteidx]
secondstr = timestr[minuteidx + 1 : -1]
elif houridx != -1:
hourstr = timestr[0:-1]
elif minuteidx != -1:
minutestr = timestr[0:-1]
elif secondidx != -1:
secondstr = timestr[0:-1]
else:
raise ISOFormatError(
'"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr)
)
for componentstr in [hourstr, minutestr, secondstr]:
if componentstr is not None:
if "." in componentstr:
intstr, fractionalstr = componentstr.split(".", 1)
if intstr.isdigit() is False:
raise ISOFormatError(
'"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr)
)
else:
if componentstr.isdigit() is False:
raise ISOFormatError(
'"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr)
)
# Parse any date components
durationdict = {"PnY": None, "PnM": None, "PnW": None, "PnD": None}
if len(datestr) > 1:
durationdict = _parse_duration_prescribed_notime(datestr)
durationdict.update({"TnH": hourstr, "TnM": minutestr, "TnS": secondstr})
return durationdict
def _parse_duration_combined(durationstr):
# Period of the form P<date>T<time>
# Split the string in to its component parts
datepart, timepart = durationstr[1:].split("T", 1) # We skip the 'P'
datevalue = parse_date(datepart, builder=TupleBuilder)
timevalue = parse_time(timepart, builder=TupleBuilder)
return {
"PnY": datevalue.YYYY,
"PnM": datevalue.MM,
"PnD": datevalue.DD,
"TnH": timevalue.hh,
"TnM": timevalue.mm,
"TnS": timevalue.ss,
}
def _has_any_component(durationstr, components):
# Given a duration string, and a list of components, returns True
# if any of the listed components are present, False otherwise.
#
# For instance:
# durationstr = 'P1Y'
# components = ['Y', 'M']
#
# returns True
#
# durationstr = 'P1Y'
# components = ['M', 'D']
#
# returns False
for component in components:
if durationstr.find(component) != -1:
return True
return False

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
class ISOFormatError(ValueError):
"""Raised when ISO 8601 string fails a format check."""
class RangeCheckError(ValueError):
"""Parent type of range check errors."""
class YearOutOfBoundsError(RangeCheckError):
"""Raised when year exceeds limits."""
class MonthOutOfBoundsError(RangeCheckError):
"""Raised when month is outside of 1..12."""
class WeekOutOfBoundsError(RangeCheckError):
"""Raised when week exceeds a year."""
class DayOutOfBoundsError(RangeCheckError):
"""Raised when day is outside of 1..365, 1..366 for leap year."""
class HoursOutOfBoundsError(RangeCheckError):
"""Raise when parsed hours are greater than 24."""
class MinutesOutOfBoundsError(RangeCheckError):
"""Raise when parsed seconds are greater than 60."""
class SecondsOutOfBoundsError(RangeCheckError):
"""Raise when parsed seconds are greater than 60."""
class MidnightBoundsError(RangeCheckError):
"""Raise when parsed time has an hour of 24 but is not midnight."""
class LeapSecondError(RangeCheckError):
"""Raised when attempting to parse a leap second"""

@ -0,0 +1,350 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
from aniso8601.builders import DatetimeTuple, DateTuple, TupleBuilder
from aniso8601.builders.python import PythonTimeBuilder
from aniso8601.compat import is_string
from aniso8601.date import parse_date
from aniso8601.duration import parse_duration
from aniso8601.exceptions import ISOFormatError
from aniso8601.resolution import IntervalResolution
from aniso8601.time import parse_datetime, parse_time
def get_interval_resolution(
isointervalstr, intervaldelimiter="/", datetimedelimiter="T"
):
isointervaltuple = parse_interval(
isointervalstr,
intervaldelimiter=intervaldelimiter,
datetimedelimiter=datetimedelimiter,
builder=TupleBuilder,
)
return _get_interval_resolution(isointervaltuple)
def get_repeating_interval_resolution(
isointervalstr, intervaldelimiter="/", datetimedelimiter="T"
):
repeatingintervaltuple = parse_repeating_interval(
isointervalstr,
intervaldelimiter=intervaldelimiter,
datetimedelimiter=datetimedelimiter,
builder=TupleBuilder,
)
return _get_interval_resolution(repeatingintervaltuple.interval)
def _get_interval_resolution(intervaltuple):
if intervaltuple.start is not None and intervaltuple.end is not None:
return max(
_get_interval_component_resolution(intervaltuple.start),
_get_interval_component_resolution(intervaltuple.end),
)
if intervaltuple.start is not None and intervaltuple.duration is not None:
return max(
_get_interval_component_resolution(intervaltuple.start),
_get_interval_component_resolution(intervaltuple.duration),
)
return max(
_get_interval_component_resolution(intervaltuple.end),
_get_interval_component_resolution(intervaltuple.duration),
)
def _get_interval_component_resolution(componenttuple):
if type(componenttuple) is DateTuple:
if componenttuple.DDD is not None:
# YYYY-DDD
# YYYYDDD
return IntervalResolution.Ordinal
if componenttuple.D is not None:
# YYYY-Www-D
# YYYYWwwD
return IntervalResolution.Weekday
if componenttuple.Www is not None:
# YYYY-Www
# YYYYWww
return IntervalResolution.Week
if componenttuple.DD is not None:
# YYYY-MM-DD
# YYYYMMDD
return IntervalResolution.Day
if componenttuple.MM is not None:
# YYYY-MM
return IntervalResolution.Month
# Y[YYY]
return IntervalResolution.Year
elif type(componenttuple) is DatetimeTuple:
# Datetime
if componenttuple.time.ss is not None:
return IntervalResolution.Seconds
if componenttuple.time.mm is not None:
return IntervalResolution.Minutes
return IntervalResolution.Hours
# Duration
if componenttuple.TnS is not None:
return IntervalResolution.Seconds
if componenttuple.TnM is not None:
return IntervalResolution.Minutes
if componenttuple.TnH is not None:
return IntervalResolution.Hours
if componenttuple.PnD is not None:
return IntervalResolution.Day
if componenttuple.PnW is not None:
return IntervalResolution.Week
if componenttuple.PnM is not None:
return IntervalResolution.Month
return IntervalResolution.Year
def parse_interval(
isointervalstr,
intervaldelimiter="/",
datetimedelimiter="T",
builder=PythonTimeBuilder,
):
# Given a string representing an ISO 8601 interval, return an
# interval built by the given builder. Valid formats are:
#
# <start>/<end>
# <start>/<duration>
# <duration>/<end>
#
# The <start> and <end> values can represent dates, or datetimes,
# not times.
#
# The format:
#
# <duration>
#
# Is expressly not supported as there is no way to provide the additional
# required context.
if is_string(isointervalstr) is False:
raise ValueError("Interval must be string.")
if len(isointervalstr) == 0:
raise ISOFormatError("Interval string is empty.")
if isointervalstr[0] == "R":
raise ISOFormatError(
"ISO 8601 repeating intervals must be parsed "
"with parse_repeating_interval."
)
intervaldelimitercount = isointervalstr.count(intervaldelimiter)
if intervaldelimitercount == 0:
raise ISOFormatError(
'Interval delimiter "{0}" is not in interval '
'string "{1}".'.format(intervaldelimiter, isointervalstr)
)
if intervaldelimitercount > 1:
raise ISOFormatError(
"{0} is not a valid ISO 8601 interval".format(isointervalstr)
)
return _parse_interval(
isointervalstr, builder, intervaldelimiter, datetimedelimiter
)
def parse_repeating_interval(
isointervalstr,
intervaldelimiter="/",
datetimedelimiter="T",
builder=PythonTimeBuilder,
):
# Given a string representing an ISO 8601 interval repeating, return an
# interval built by the given builder. Valid formats are:
#
# Rnn/<interval>
# R/<interval>
if not isinstance(isointervalstr, str):
raise ValueError("Interval must be string.")
if len(isointervalstr) == 0:
raise ISOFormatError("Repeating interval string is empty.")
if isointervalstr[0] != "R":
raise ISOFormatError("ISO 8601 repeating interval must start " "with an R.")
if intervaldelimiter not in isointervalstr:
raise ISOFormatError(
'Interval delimiter "{0}" is not in interval '
'string "{1}".'.format(intervaldelimiter, isointervalstr)
)
# Parse the number of iterations
iterationpart, intervalpart = isointervalstr.split(intervaldelimiter, 1)
if len(iterationpart) > 1:
R = False
Rnn = iterationpart[1:]
else:
R = True
Rnn = None
interval = _parse_interval(
intervalpart, TupleBuilder, intervaldelimiter, datetimedelimiter
)
return builder.build_repeating_interval(R=R, Rnn=Rnn, interval=interval)
def _parse_interval(
isointervalstr, builder, intervaldelimiter="/", datetimedelimiter="T"
):
# Returns a tuple containing the start of the interval, the end of the
# interval, and or the interval duration
firstpart, secondpart = isointervalstr.split(intervaldelimiter)
if len(firstpart) == 0 or len(secondpart) == 0:
raise ISOFormatError(
"{0} is not a valid ISO 8601 interval".format(isointervalstr)
)
if firstpart[0] == "P":
# <duration>/<end>
# Notice that these are not returned 'in order' (earlier to later), this
# is to maintain consistency with parsing <start>/<end> durations, as
# well as making repeating interval code cleaner. Users who desire
# durations to be in order can use the 'sorted' operator.
duration = parse_duration(firstpart, builder=TupleBuilder)
# We need to figure out if <end> is a date, or a datetime
if secondpart.find(datetimedelimiter) != -1:
# <end> is a datetime
endtuple = parse_datetime(
secondpart, delimiter=datetimedelimiter, builder=TupleBuilder
)
else:
endtuple = parse_date(secondpart, builder=TupleBuilder)
return builder.build_interval(end=endtuple, duration=duration)
elif secondpart[0] == "P":
# <start>/<duration>
# We need to figure out if <start> is a date, or a datetime
duration = parse_duration(secondpart, builder=TupleBuilder)
if firstpart.find(datetimedelimiter) != -1:
# <start> is a datetime
starttuple = parse_datetime(
firstpart, delimiter=datetimedelimiter, builder=TupleBuilder
)
else:
# <start> must just be a date
starttuple = parse_date(firstpart, builder=TupleBuilder)
return builder.build_interval(start=starttuple, duration=duration)
# <start>/<end>
if firstpart.find(datetimedelimiter) != -1:
# Both parts are datetimes
starttuple = parse_datetime(
firstpart, delimiter=datetimedelimiter, builder=TupleBuilder
)
else:
starttuple = parse_date(firstpart, builder=TupleBuilder)
endtuple = _parse_interval_end(secondpart, starttuple, datetimedelimiter)
return builder.build_interval(start=starttuple, end=endtuple)
def _parse_interval_end(endstr, starttuple, datetimedelimiter):
datestr = None
timestr = None
monthstr = None
daystr = None
concise = False
if type(starttuple) is DateTuple:
startdatetuple = starttuple
else:
# Start is a datetime
startdatetuple = starttuple.date
if datetimedelimiter in endstr:
datestr, timestr = endstr.split(datetimedelimiter, 1)
elif ":" in endstr:
timestr = endstr
else:
datestr = endstr
if timestr is not None:
endtimetuple = parse_time(timestr, builder=TupleBuilder)
# End is just a time
if datestr is None:
return endtimetuple
# Handle backwards concise representation
if datestr.count("-") == 1:
monthstr, daystr = datestr.split("-")
concise = True
elif len(datestr) <= 2:
daystr = datestr
concise = True
elif len(datestr) <= 4:
monthstr = datestr[0:2]
daystr = datestr[2:]
concise = True
if concise is True:
concisedatestr = startdatetuple.YYYY
# Separators required because concise elements may be missing digits
if monthstr is not None:
concisedatestr += "-" + monthstr
elif startdatetuple.MM is not None:
concisedatestr += "-" + startdatetuple.MM
concisedatestr += "-" + daystr
enddatetuple = parse_date(concisedatestr, builder=TupleBuilder)
# Clear unsupplied components
if monthstr is None:
enddatetuple = TupleBuilder.build_date(DD=enddatetuple.DD)
else:
# Year not provided
enddatetuple = TupleBuilder.build_date(
MM=enddatetuple.MM, DD=enddatetuple.DD
)
else:
enddatetuple = parse_date(datestr, builder=TupleBuilder)
if timestr is None:
return enddatetuple
return TupleBuilder.build_datetime(enddatetuple, endtimetuple)

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
from aniso8601 import compat
class DateResolution(object):
Year, Month, Week, Weekday, Day, Ordinal = list(compat.range(6))
class DurationResolution(object):
Years, Months, Weeks, Days, Hours, Minutes, Seconds = list(compat.range(7))
class IntervalResolution(object):
Year, Month, Week, Weekday, Day, Ordinal, Hours, Minutes, Seconds = list(
compat.range(9)
)
class TimeResolution(object):
Seconds, Minutes, Hours = list(compat.range(3))

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
import sys
PY2 = sys.version_info[0] == 2
if PY2:
import mock # pylint: disable=import-error
else:
from unittest import mock

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
import unittest
from aniso8601.compat import PY2, is_string
class TestCompatFunctions(unittest.TestCase):
def test_is_string(self):
self.assertTrue(is_string("asdf"))
self.assertTrue(is_string(""))
# pylint: disable=undefined-variable
if PY2 is True:
self.assertTrue(is_string(unicode("asdf")))
self.assertFalse(is_string(None))
self.assertFalse(is_string(123))
self.assertFalse(is_string(4.56))
self.assertFalse(is_string([]))
self.assertFalse(is_string({}))

@ -0,0 +1,303 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
import unittest
import aniso8601
from aniso8601.date import get_date_resolution, parse_date
from aniso8601.exceptions import DayOutOfBoundsError, ISOFormatError
from aniso8601.resolution import DateResolution
from aniso8601.tests.compat import mock
class TestDateResolutionFunctions(unittest.TestCase):
def test_get_date_resolution_year(self):
self.assertEqual(get_date_resolution("2013"), DateResolution.Year)
self.assertEqual(get_date_resolution("0001"), DateResolution.Year)
self.assertEqual(get_date_resolution("19"), DateResolution.Year)
def test_get_date_resolution_month(self):
self.assertEqual(get_date_resolution("1981-04"), DateResolution.Month)
def test_get_date_resolution_week(self):
self.assertEqual(get_date_resolution("2004-W53"), DateResolution.Week)
self.assertEqual(get_date_resolution("2009-W01"), DateResolution.Week)
self.assertEqual(get_date_resolution("2004W53"), DateResolution.Week)
def test_get_date_resolution_day(self):
self.assertEqual(get_date_resolution("2004-04-11"), DateResolution.Day)
self.assertEqual(get_date_resolution("20090121"), DateResolution.Day)
def test_get_date_resolution_year_weekday(self):
self.assertEqual(get_date_resolution("2004-W53-6"), DateResolution.Weekday)
self.assertEqual(get_date_resolution("2004W536"), DateResolution.Weekday)
def test_get_date_resolution_year_ordinal(self):
self.assertEqual(get_date_resolution("1981-095"), DateResolution.Ordinal)
self.assertEqual(get_date_resolution("1981095"), DateResolution.Ordinal)
def test_get_date_resolution_badtype(self):
testtuples = (None, 1, False, 1.234)
for testtuple in testtuples:
with self.assertRaises(ValueError):
get_date_resolution(testtuple)
def test_get_date_resolution_extended_year(self):
testtuples = ("+2000", "+30000")
for testtuple in testtuples:
with self.assertRaises(NotImplementedError):
get_date_resolution(testtuple)
def test_get_date_resolution_badweek(self):
testtuples = ("2004-W1", "2004W1")
for testtuple in testtuples:
with self.assertRaises(ISOFormatError):
get_date_resolution(testtuple)
def test_get_date_resolution_badweekday(self):
testtuples = ("2004-W53-67", "2004W5367")
for testtuple in testtuples:
with self.assertRaises(ISOFormatError):
get_date_resolution(testtuple)
def test_get_date_resolution_badstr(self):
testtuples = (
"W53",
"2004-W",
"2014-01-230",
"2014-012-23",
"201-01-23",
"201401230",
"201401",
"",
)
for testtuple in testtuples:
with self.assertRaises(ISOFormatError):
get_date_resolution(testtuple)
class TestDateParserFunctions(unittest.TestCase):
def test_parse_date(self):
testtuples = (
(
"2013",
{
"YYYY": "2013",
"MM": None,
"DD": None,
"Www": None,
"D": None,
"DDD": None,
},
),
(
"0001",
{
"YYYY": "0001",
"MM": None,
"DD": None,
"Www": None,
"D": None,
"DDD": None,
},
),
(
"19",
{
"YYYY": "19",
"MM": None,
"DD": None,
"Www": None,
"D": None,
"DDD": None,
},
),
(
"1981-04-05",
{
"YYYY": "1981",
"MM": "04",
"DD": "05",
"Www": None,
"D": None,
"DDD": None,
},
),
(
"19810405",
{
"YYYY": "1981",
"MM": "04",
"DD": "05",
"Www": None,
"D": None,
"DDD": None,
},
),
(
"1981-04",
{
"YYYY": "1981",
"MM": "04",
"DD": None,
"Www": None,
"D": None,
"DDD": None,
},
),
(
"2004-W53",
{
"YYYY": "2004",
"MM": None,
"DD": None,
"Www": "53",
"D": None,
"DDD": None,
},
),
(
"2009-W01",
{
"YYYY": "2009",
"MM": None,
"DD": None,
"Www": "01",
"D": None,
"DDD": None,
},
),
(
"2004-W53-6",
{
"YYYY": "2004",
"MM": None,
"DD": None,
"Www": "53",
"D": "6",
"DDD": None,
},
),
(
"2004W53",
{
"YYYY": "2004",
"MM": None,
"DD": None,
"Www": "53",
"D": None,
"DDD": None,
},
),
(
"2004W536",
{
"YYYY": "2004",
"MM": None,
"DD": None,
"Www": "53",
"D": "6",
"DDD": None,
},
),
(
"1981-095",
{
"YYYY": "1981",
"MM": None,
"DD": None,
"Www": None,
"D": None,
"DDD": "095",
},
),
(
"1981095",
{
"YYYY": "1981",
"MM": None,
"DD": None,
"Www": None,
"D": None,
"DDD": "095",
},
),
(
"1980366",
{
"YYYY": "1980",
"MM": None,
"DD": None,
"Www": None,
"D": None,
"DDD": "366",
},
),
)
for testtuple in testtuples:
with mock.patch.object(
aniso8601.date.PythonTimeBuilder, "build_date"
) as mockBuildDate:
mockBuildDate.return_value = testtuple[1]
result = parse_date(testtuple[0])
self.assertEqual(result, testtuple[1])
mockBuildDate.assert_called_once_with(**testtuple[1])
def test_parse_date_badtype(self):
testtuples = (None, 1, False, 1.234)
for testtuple in testtuples:
with self.assertRaises(ValueError):
parse_date(testtuple, builder=None)
def test_parse_date_badstr(self):
testtuples = (
"W53",
"2004-W",
"2014-01-230",
"2014-012-23",
"201-01-23",
"201401230",
"201401",
"9999 W53",
"20.50230",
"198104",
"bad",
"",
)
for testtuple in testtuples:
with self.assertRaises(ISOFormatError):
parse_date(testtuple, builder=None)
def test_parse_date_mockbuilder(self):
mockBuilder = mock.Mock()
expectedargs = {
"YYYY": "1981",
"MM": "04",
"DD": "05",
"Www": None,
"D": None,
"DDD": None,
}
mockBuilder.build_date.return_value = expectedargs
result = parse_date("1981-04-05", builder=mockBuilder)
self.assertEqual(result, expectedargs)
mockBuilder.build_date.assert_called_once_with(**expectedargs)

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
import unittest
from aniso8601.decimalfraction import normalize
class TestDecimalFractionFunctions(unittest.TestCase):
def test_normalize(self):
self.assertEqual(normalize(""), "")
self.assertEqual(normalize("12.34"), "12.34")
self.assertEqual(normalize("123,45"), "123.45")
self.assertEqual(normalize("123,45,67"), "123.45.67")

File diff suppressed because it is too large Load Diff

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
import unittest
import aniso8601
class TestInitFunctions(unittest.TestCase):
def test_import(self):
# Verify the function mappings
self.assertEqual(aniso8601.parse_datetime, aniso8601.time.parse_datetime)
self.assertEqual(aniso8601.parse_time, aniso8601.time.parse_time)
self.assertEqual(
aniso8601.get_time_resolution, aniso8601.time.get_time_resolution
)
self.assertEqual(
aniso8601.get_datetime_resolution, aniso8601.time.get_datetime_resolution
)
self.assertEqual(aniso8601.parse_date, aniso8601.date.parse_date)
self.assertEqual(
aniso8601.get_date_resolution, aniso8601.date.get_date_resolution
)
self.assertEqual(aniso8601.parse_duration, aniso8601.duration.parse_duration)
self.assertEqual(
aniso8601.get_duration_resolution,
aniso8601.duration.get_duration_resolution,
)
self.assertEqual(aniso8601.parse_interval, aniso8601.interval.parse_interval)
self.assertEqual(
aniso8601.parse_repeating_interval,
aniso8601.interval.parse_repeating_interval,
)
self.assertEqual(
aniso8601.get_interval_resolution,
aniso8601.interval.get_interval_resolution,
)
self.assertEqual(
aniso8601.get_repeating_interval_resolution,
aniso8601.interval.get_repeating_interval_resolution,
)

File diff suppressed because it is too large Load Diff

@ -0,0 +1,539 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
import unittest
import aniso8601
from aniso8601.builders import DatetimeTuple, DateTuple, TimeTuple, TimezoneTuple
from aniso8601.exceptions import ISOFormatError
from aniso8601.resolution import TimeResolution
from aniso8601.tests.compat import mock
from aniso8601.time import (
_get_time_resolution,
get_datetime_resolution,
get_time_resolution,
parse_datetime,
parse_time,
)
class TestTimeResolutionFunctions(unittest.TestCase):
def test_get_time_resolution(self):
self.assertEqual(get_time_resolution("01:23:45"), TimeResolution.Seconds)
self.assertEqual(get_time_resolution("24:00:00"), TimeResolution.Seconds)
self.assertEqual(get_time_resolution("23:21:28,512400"), TimeResolution.Seconds)
self.assertEqual(get_time_resolution("23:21:28.512400"), TimeResolution.Seconds)
self.assertEqual(get_time_resolution("01:23"), TimeResolution.Minutes)
self.assertEqual(get_time_resolution("24:00"), TimeResolution.Minutes)
self.assertEqual(get_time_resolution("01:23,4567"), TimeResolution.Minutes)
self.assertEqual(get_time_resolution("01:23.4567"), TimeResolution.Minutes)
self.assertEqual(get_time_resolution("012345"), TimeResolution.Seconds)
self.assertEqual(get_time_resolution("240000"), TimeResolution.Seconds)
self.assertEqual(get_time_resolution("0123"), TimeResolution.Minutes)
self.assertEqual(get_time_resolution("2400"), TimeResolution.Minutes)
self.assertEqual(get_time_resolution("01"), TimeResolution.Hours)
self.assertEqual(get_time_resolution("24"), TimeResolution.Hours)
self.assertEqual(get_time_resolution("12,5"), TimeResolution.Hours)
self.assertEqual(get_time_resolution("12.5"), TimeResolution.Hours)
self.assertEqual(
get_time_resolution("232128.512400+00:00"), TimeResolution.Seconds
)
self.assertEqual(get_time_resolution("0123.4567+00:00"), TimeResolution.Minutes)
self.assertEqual(get_time_resolution("01.4567+00:00"), TimeResolution.Hours)
self.assertEqual(get_time_resolution("01:23:45+00:00"), TimeResolution.Seconds)
self.assertEqual(get_time_resolution("24:00:00+00:00"), TimeResolution.Seconds)
self.assertEqual(
get_time_resolution("23:21:28.512400+00:00"), TimeResolution.Seconds
)
self.assertEqual(get_time_resolution("01:23+00:00"), TimeResolution.Minutes)
self.assertEqual(get_time_resolution("24:00+00:00"), TimeResolution.Minutes)
self.assertEqual(
get_time_resolution("01:23.4567+00:00"), TimeResolution.Minutes
)
self.assertEqual(
get_time_resolution("23:21:28.512400+11:15"), TimeResolution.Seconds
)
self.assertEqual(
get_time_resolution("23:21:28.512400-12:34"), TimeResolution.Seconds
)
self.assertEqual(
get_time_resolution("23:21:28.512400Z"), TimeResolution.Seconds
)
self.assertEqual(
get_time_resolution("06:14:00.000123Z"), TimeResolution.Seconds
)
def test_get_datetime_resolution(self):
self.assertEqual(
get_datetime_resolution("2019-06-05T01:03:11.858714"),
TimeResolution.Seconds,
)
self.assertEqual(
get_datetime_resolution("2019-06-05T01:03:11"), TimeResolution.Seconds
)
self.assertEqual(
get_datetime_resolution("2019-06-05T01:03"), TimeResolution.Minutes
)
self.assertEqual(get_datetime_resolution("2019-06-05T01"), TimeResolution.Hours)
def test_get_time_resolution_badtype(self):
testtuples = (None, 1, False, 1.234)
for testtuple in testtuples:
with self.assertRaises(ValueError):
get_time_resolution(testtuple)
def test_get_time_resolution_badstr(self):
testtuples = ("A6:14:00.000123Z", "06:14:0B", "bad", "")
for testtuple in testtuples:
with self.assertRaises(ISOFormatError):
get_time_resolution(testtuple)
def test_get_time_resolution_internal(self):
self.assertEqual(
_get_time_resolution(TimeTuple(hh="01", mm="02", ss="03", tz=None)),
TimeResolution.Seconds,
)
self.assertEqual(
_get_time_resolution(TimeTuple(hh="01", mm="02", ss=None, tz=None)),
TimeResolution.Minutes,
)
self.assertEqual(
_get_time_resolution(TimeTuple(hh="01", mm=None, ss=None, tz=None)),
TimeResolution.Hours,
)
class TestTimeParserFunctions(unittest.TestCase):
def test_parse_time(self):
testtuples = (
("01:23:45", {"hh": "01", "mm": "23", "ss": "45", "tz": None}),
("24:00:00", {"hh": "24", "mm": "00", "ss": "00", "tz": None}),
(
"23:21:28,512400",
{"hh": "23", "mm": "21", "ss": "28.512400", "tz": None},
),
(
"23:21:28.512400",
{"hh": "23", "mm": "21", "ss": "28.512400", "tz": None},
),
(
"01:03:11.858714",
{"hh": "01", "mm": "03", "ss": "11.858714", "tz": None},
),
(
"14:43:59.9999997",
{"hh": "14", "mm": "43", "ss": "59.9999997", "tz": None},
),
("01:23", {"hh": "01", "mm": "23", "ss": None, "tz": None}),
("24:00", {"hh": "24", "mm": "00", "ss": None, "tz": None}),
("01:23,4567", {"hh": "01", "mm": "23.4567", "ss": None, "tz": None}),
("01:23.4567", {"hh": "01", "mm": "23.4567", "ss": None, "tz": None}),
("012345", {"hh": "01", "mm": "23", "ss": "45", "tz": None}),
("240000", {"hh": "24", "mm": "00", "ss": "00", "tz": None}),
("232128,512400", {"hh": "23", "mm": "21", "ss": "28.512400", "tz": None}),
("232128.512400", {"hh": "23", "mm": "21", "ss": "28.512400", "tz": None}),
("010311.858714", {"hh": "01", "mm": "03", "ss": "11.858714", "tz": None}),
(
"144359.9999997",
{"hh": "14", "mm": "43", "ss": "59.9999997", "tz": None},
),
("0123", {"hh": "01", "mm": "23", "ss": None, "tz": None}),
("2400", {"hh": "24", "mm": "00", "ss": None, "tz": None}),
("01", {"hh": "01", "mm": None, "ss": None, "tz": None}),
("24", {"hh": "24", "mm": None, "ss": None, "tz": None}),
("12,5", {"hh": "12.5", "mm": None, "ss": None, "tz": None}),
("12.5", {"hh": "12.5", "mm": None, "ss": None, "tz": None}),
(
"232128,512400+00:00",
{
"hh": "23",
"mm": "21",
"ss": "28.512400",
"tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
},
),
(
"232128.512400+00:00",
{
"hh": "23",
"mm": "21",
"ss": "28.512400",
"tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
},
),
(
"0123,4567+00:00",
{
"hh": "01",
"mm": "23.4567",
"ss": None,
"tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
},
),
(
"0123.4567+00:00",
{
"hh": "01",
"mm": "23.4567",
"ss": None,
"tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
},
),
(
"01,4567+00:00",
{
"hh": "01.4567",
"mm": None,
"ss": None,
"tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
},
),
(
"01.4567+00:00",
{
"hh": "01.4567",
"mm": None,
"ss": None,
"tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
},
),
(
"01:23:45+00:00",
{
"hh": "01",
"mm": "23",
"ss": "45",
"tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
},
),
(
"24:00:00+00:00",
{
"hh": "24",
"mm": "00",
"ss": "00",
"tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
},
),
(
"23:21:28.512400+00:00",
{
"hh": "23",
"mm": "21",
"ss": "28.512400",
"tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
},
),
(
"01:23+00:00",
{
"hh": "01",
"mm": "23",
"ss": None,
"tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
},
),
(
"24:00+00:00",
{
"hh": "24",
"mm": "00",
"ss": None,
"tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
},
),
(
"01:23.4567+00:00",
{
"hh": "01",
"mm": "23.4567",
"ss": None,
"tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
},
),
(
"23:21:28.512400+11:15",
{
"hh": "23",
"mm": "21",
"ss": "28.512400",
"tz": TimezoneTuple(False, None, "11", "15", "+11:15"),
},
),
(
"23:21:28.512400-12:34",
{
"hh": "23",
"mm": "21",
"ss": "28.512400",
"tz": TimezoneTuple(True, None, "12", "34", "-12:34"),
},
),
(
"23:21:28.512400Z",
{
"hh": "23",
"mm": "21",
"ss": "28.512400",
"tz": TimezoneTuple(False, True, None, None, "Z"),
},
),
(
"06:14:00.000123Z",
{
"hh": "06",
"mm": "14",
"ss": "00.000123",
"tz": TimezoneTuple(False, True, None, None, "Z"),
},
),
)
for testtuple in testtuples:
with mock.patch.object(
aniso8601.time.PythonTimeBuilder, "build_time"
) as mockBuildTime:
mockBuildTime.return_value = testtuple[1]
result = parse_time(testtuple[0])
self.assertEqual(result, testtuple[1])
mockBuildTime.assert_called_once_with(**testtuple[1])
def test_parse_time_badtype(self):
testtuples = (None, 1, False, 1.234)
for testtuple in testtuples:
with self.assertRaises(ValueError):
parse_time(testtuple, builder=None)
def test_parse_time_badstr(self):
testtuples = (
"A6:14:00.000123Z",
"06:14:0B",
"06:1 :02",
"0000,70:24,9",
"00.27:5332",
"bad",
"",
)
for testtuple in testtuples:
with self.assertRaises(ISOFormatError):
parse_time(testtuple, builder=None)
def test_parse_time_mockbuilder(self):
mockBuilder = mock.Mock()
expectedargs = {"hh": "01", "mm": "23", "ss": "45", "tz": None}
mockBuilder.build_time.return_value = expectedargs
result = parse_time("01:23:45", builder=mockBuilder)
self.assertEqual(result, expectedargs)
mockBuilder.build_time.assert_called_once_with(**expectedargs)
mockBuilder = mock.Mock()
expectedargs = {
"hh": "23",
"mm": "21",
"ss": "28.512400",
"tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
}
mockBuilder.build_time.return_value = expectedargs
result = parse_time("232128.512400+00:00", builder=mockBuilder)
self.assertEqual(result, expectedargs)
mockBuilder.build_time.assert_called_once_with(**expectedargs)
mockBuilder = mock.Mock()
expectedargs = {
"hh": "23",
"mm": "21",
"ss": "28.512400",
"tz": TimezoneTuple(False, None, "11", "15", "+11:15"),
}
mockBuilder.build_time.return_value = expectedargs
result = parse_time("23:21:28.512400+11:15", builder=mockBuilder)
self.assertEqual(result, expectedargs)
mockBuilder.build_time.assert_called_once_with(**expectedargs)
def test_parse_datetime(self):
testtuples = (
(
"2019-06-05T01:03:11,858714",
(
DateTuple("2019", "06", "05", None, None, None),
TimeTuple("01", "03", "11.858714", None),
),
),
(
"2019-06-05T01:03:11.858714",
(
DateTuple("2019", "06", "05", None, None, None),
TimeTuple("01", "03", "11.858714", None),
),
),
(
"1981-04-05T23:21:28.512400Z",
(
DateTuple("1981", "04", "05", None, None, None),
TimeTuple(
"23",
"21",
"28.512400",
TimezoneTuple(False, True, None, None, "Z"),
),
),
),
(
"1981095T23:21:28.512400-12:34",
(
DateTuple("1981", None, None, None, None, "095"),
TimeTuple(
"23",
"21",
"28.512400",
TimezoneTuple(True, None, "12", "34", "-12:34"),
),
),
),
(
"19810405T23:21:28+00",
(
DateTuple("1981", "04", "05", None, None, None),
TimeTuple(
"23", "21", "28", TimezoneTuple(False, None, "00", None, "+00")
),
),
),
(
"19810405T23:21:28+00:00",
(
DateTuple("1981", "04", "05", None, None, None),
TimeTuple(
"23",
"21",
"28",
TimezoneTuple(False, None, "00", "00", "+00:00"),
),
),
),
)
for testtuple in testtuples:
with mock.patch.object(
aniso8601.time.PythonTimeBuilder, "build_datetime"
) as mockBuildDateTime:
mockBuildDateTime.return_value = testtuple[1]
result = parse_datetime(testtuple[0])
self.assertEqual(result, testtuple[1])
mockBuildDateTime.assert_called_once_with(*testtuple[1])
def test_parse_datetime_spacedelimited(self):
expectedargs = (
DateTuple("2004", None, None, "53", "6", None),
TimeTuple(
"23", "21", "28.512400", TimezoneTuple(True, None, "12", "34", "-12:34")
),
)
with mock.patch.object(
aniso8601.time.PythonTimeBuilder, "build_datetime"
) as mockBuildDateTime:
mockBuildDateTime.return_value = expectedargs
result = parse_datetime("2004-W53-6 23:21:28.512400-12:34", delimiter=" ")
self.assertEqual(result, expectedargs)
mockBuildDateTime.assert_called_once_with(*expectedargs)
def test_parse_datetime_commadelimited(self):
expectedargs = (
DateTuple("1981", "04", "05", None, None, None),
TimeTuple(
"23", "21", "28.512400", TimezoneTuple(False, True, None, None, "Z")
),
)
with mock.patch.object(
aniso8601.time.PythonTimeBuilder, "build_datetime"
) as mockBuildDateTime:
mockBuildDateTime.return_value = expectedargs
result = parse_datetime("1981-04-05,23:21:28,512400Z", delimiter=",")
self.assertEqual(result, expectedargs)
mockBuildDateTime.assert_called_once_with(*expectedargs)
def test_parse_datetime_baddelimiter(self):
testtuples = (
"1981-04-05,23:21:28,512400Z",
"2004-W53-6 23:21:28.512400-12:3",
"1981040523:21:28",
)
for testtuple in testtuples:
with self.assertRaises(ISOFormatError):
parse_datetime(testtuple, builder=None)
def test_parse_datetime_badtype(self):
testtuples = (None, 1, False, 1.234)
for testtuple in testtuples:
with self.assertRaises(ValueError):
parse_datetime(testtuple, builder=None)
def test_parse_datetime_badstr(self):
testtuples = (
"1981-04-05TA6:14:00.000123Z",
"2004-W53-6T06:14:0B",
"2014-01-230T23:21:28+00",
"201401230T01:03:11.858714",
"9999 W53T49",
"9T0000,70:24,9",
"bad",
"",
)
for testtuple in testtuples:
with self.assertRaises(ISOFormatError):
parse_datetime(testtuple, builder=None)
def test_parse_datetime_mockbuilder(self):
mockBuilder = mock.Mock()
expectedargs = (
DateTuple("1981", None, None, None, None, "095"),
TimeTuple(
"23", "21", "28.512400", TimezoneTuple(True, None, "12", "34", "-12:34")
),
)
mockBuilder.build_datetime.return_value = expectedargs
result = parse_datetime("1981095T23:21:28.512400-12:34", builder=mockBuilder)
self.assertEqual(result, expectedargs)
mockBuilder.build_datetime.assert_called_once_with(*expectedargs)

@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
import unittest
import aniso8601
from aniso8601.exceptions import ISOFormatError
from aniso8601.tests.compat import mock
from aniso8601.timezone import parse_timezone
class TestTimezoneParserFunctions(unittest.TestCase):
def test_parse_timezone(self):
testtuples = (
("Z", {"negative": False, "Z": True, "name": "Z"}),
("+00:00", {"negative": False, "hh": "00", "mm": "00", "name": "+00:00"}),
("+01:00", {"negative": False, "hh": "01", "mm": "00", "name": "+01:00"}),
("-01:00", {"negative": True, "hh": "01", "mm": "00", "name": "-01:00"}),
("+00:12", {"negative": False, "hh": "00", "mm": "12", "name": "+00:12"}),
("+01:23", {"negative": False, "hh": "01", "mm": "23", "name": "+01:23"}),
("-01:23", {"negative": True, "hh": "01", "mm": "23", "name": "-01:23"}),
("+0000", {"negative": False, "hh": "00", "mm": "00", "name": "+0000"}),
("+0100", {"negative": False, "hh": "01", "mm": "00", "name": "+0100"}),
("-0100", {"negative": True, "hh": "01", "mm": "00", "name": "-0100"}),
("+0012", {"negative": False, "hh": "00", "mm": "12", "name": "+0012"}),
("+0123", {"negative": False, "hh": "01", "mm": "23", "name": "+0123"}),
("-0123", {"negative": True, "hh": "01", "mm": "23", "name": "-0123"}),
("+00", {"negative": False, "hh": "00", "mm": None, "name": "+00"}),
("+01", {"negative": False, "hh": "01", "mm": None, "name": "+01"}),
("-01", {"negative": True, "hh": "01", "mm": None, "name": "-01"}),
("+12", {"negative": False, "hh": "12", "mm": None, "name": "+12"}),
("-12", {"negative": True, "hh": "12", "mm": None, "name": "-12"}),
)
for testtuple in testtuples:
with mock.patch.object(
aniso8601.timezone.PythonTimeBuilder, "build_timezone"
) as mockBuildTimezone:
mockBuildTimezone.return_value = testtuple[1]
result = parse_timezone(testtuple[0])
self.assertEqual(result, testtuple[1])
mockBuildTimezone.assert_called_once_with(**testtuple[1])
def test_parse_timezone_badtype(self):
testtuples = (None, 1, False, 1.234)
for testtuple in testtuples:
with self.assertRaises(ValueError):
parse_timezone(testtuple, builder=None)
def test_parse_timezone_badstr(self):
testtuples = (
"+1",
"-00",
"-0000",
"-00:00",
"01",
"0123",
"@12:34",
"Y",
" Z",
"Z ",
" Z ",
"bad",
"",
)
for testtuple in testtuples:
with self.assertRaises(ISOFormatError):
parse_timezone(testtuple, builder=None)
def test_parse_timezone_mockbuilder(self):
mockBuilder = mock.Mock()
expectedargs = {"negative": False, "Z": True, "name": "Z"}
mockBuilder.build_timezone.return_value = expectedargs
result = parse_timezone("Z", builder=mockBuilder)
self.assertEqual(result, expectedargs)
mockBuilder.build_timezone.assert_called_once_with(**expectedargs)
mockBuilder = mock.Mock()
expectedargs = {"negative": False, "hh": "00", "mm": "00", "name": "+00:00"}
mockBuilder.build_timezone.return_value = expectedargs
result = parse_timezone("+00:00", builder=mockBuilder)
self.assertEqual(result, expectedargs)
mockBuilder.build_timezone.assert_called_once_with(**expectedargs)
mockBuilder = mock.Mock()
expectedargs = {"negative": True, "hh": "01", "mm": "23", "name": "-01:23"}
mockBuilder.build_timezone.return_value = expectedargs
result = parse_timezone("-01:23", builder=mockBuilder)
self.assertEqual(result, expectedargs)
mockBuilder.build_timezone.assert_called_once_with(**expectedargs)
def test_parse_timezone_negativezero(self):
# A 0 offset cannot be negative
with self.assertRaises(ISOFormatError):
parse_timezone("-00:00", builder=None)
with self.assertRaises(ISOFormatError):
parse_timezone("-0000", builder=None)
with self.assertRaises(ISOFormatError):
parse_timezone("-00", builder=None)

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
import datetime
import pickle
import unittest
from aniso8601.utcoffset import UTCOffset
class TestUTCOffset(unittest.TestCase):
def test_pickle(self):
# Make sure timezone objects are pickleable
testutcoffset = UTCOffset(name="UTC", minutes=0)
utcoffsetpickle = pickle.dumps(testutcoffset)
resultutcoffset = pickle.loads(utcoffsetpickle)
self.assertEqual(resultutcoffset._name, testutcoffset._name)
self.assertEqual(resultutcoffset._utcdelta, testutcoffset._utcdelta)
def test_repr(self):
self.assertEqual(str(UTCOffset(minutes=0)), "+0:00:00 UTC")
self.assertEqual(str(UTCOffset(minutes=60)), "+1:00:00 UTC")
self.assertEqual(str(UTCOffset(minutes=-60)), "-1:00:00 UTC")
self.assertEqual(str(UTCOffset(minutes=12)), "+0:12:00 UTC")
self.assertEqual(str(UTCOffset(minutes=-12)), "-0:12:00 UTC")
self.assertEqual(str(UTCOffset(minutes=83)), "+1:23:00 UTC")
self.assertEqual(str(UTCOffset(minutes=-83)), "-1:23:00 UTC")
self.assertEqual(str(UTCOffset(minutes=1440)), "+1 day, 0:00:00 UTC")
self.assertEqual(str(UTCOffset(minutes=-1440)), "-1 day, 0:00:00 UTC")
self.assertEqual(str(UTCOffset(minutes=2967)), "+2 days, 1:27:00 UTC")
self.assertEqual(str(UTCOffset(minutes=-2967)), "-2 days, 1:27:00 UTC")
def test_dst(self):
tzinfoobject = UTCOffset(minutes=240)
# This would raise ISOFormatError or a TypeError if dst info is invalid
result = datetime.datetime.now(tzinfoobject)
# Hacky way to make sure the tzinfo is what we'd expect
self.assertEqual(result.tzinfo.utcoffset(None), datetime.timedelta(hours=4))

@ -0,0 +1,203 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
from aniso8601.builders import TupleBuilder
from aniso8601.builders.python import PythonTimeBuilder
from aniso8601.compat import is_string
from aniso8601.date import parse_date
from aniso8601.decimalfraction import normalize
from aniso8601.exceptions import ISOFormatError
from aniso8601.resolution import TimeResolution
from aniso8601.timezone import parse_timezone
TIMEZONE_DELIMITERS = ["Z", "+", "-"]
def get_time_resolution(isotimestr):
# Valid time formats are:
#
# hh:mm:ss
# hhmmss
# hh:mm
# hhmm
# hh
# hh:mm:ssZ
# hhmmssZ
# hh:mmZ
# hhmmZ
# hhZ
# hh:mm:ss±hh:mm
# hhmmss±hh:mm
# hh:mm±hh:mm
# hhmm±hh:mm
# hh±hh:mm
# hh:mm:ss±hhmm
# hhmmss±hhmm
# hh:mm±hhmm
# hhmm±hhmm
# hh±hhmm
# hh:mm:ss±hh
# hhmmss±hh
# hh:mm±hh
# hhmm±hh
# hh±hh
isotimetuple = parse_time(isotimestr, builder=TupleBuilder)
return _get_time_resolution(isotimetuple)
def get_datetime_resolution(isodatetimestr, delimiter="T"):
# <date>T<time>
#
# Time part cannot be omittted so return time resolution
isotimetuple = parse_datetime(
isodatetimestr, delimiter=delimiter, builder=TupleBuilder
).time
return _get_time_resolution(isotimetuple)
def _get_time_resolution(isotimetuple):
if isotimetuple.ss is not None:
return TimeResolution.Seconds
if isotimetuple.mm is not None:
return TimeResolution.Minutes
return TimeResolution.Hours
def parse_time(isotimestr, builder=PythonTimeBuilder):
# Given a string in any ISO 8601 time format, return a datetime.time object
# that corresponds to the given time. Fixed offset tzdata will be included
# if UTC offset is given in the input string. Valid time formats are:
#
# hh:mm:ss
# hhmmss
# hh:mm
# hhmm
# hh
# hh:mm:ssZ
# hhmmssZ
# hh:mmZ
# hhmmZ
# hhZ
# hh:mm:ss±hh:mm
# hhmmss±hh:mm
# hh:mm±hh:mm
# hhmm±hh:mm
# hh±hh:mm
# hh:mm:ss±hhmm
# hhmmss±hhmm
# hh:mm±hhmm
# hhmm±hhmm
# hh±hhmm
# hh:mm:ss±hh
# hhmmss±hh
# hh:mm±hh
# hhmm±hh
# hh±hh
if is_string(isotimestr) is False:
raise ValueError("Time must be string.")
if len(isotimestr) == 0:
raise ISOFormatError('"{0}" is not a valid ISO 8601 time.'.format(isotimestr))
timestr = normalize(isotimestr)
hourstr = None
minutestr = None
secondstr = None
tzstr = None
fractionalstr = None
# Split out the timezone
for delimiter in TIMEZONE_DELIMITERS:
delimiteridx = timestr.find(delimiter)
if delimiteridx != -1:
tzstr = timestr[delimiteridx:]
timestr = timestr[0:delimiteridx]
# Split out the fractional component
if timestr.find(".") != -1:
timestr, fractionalstr = timestr.split(".", 1)
if fractionalstr.isdigit() is False:
raise ISOFormatError(
'"{0}" is not a valid ISO 8601 time.'.format(isotimestr)
)
if len(timestr) == 2:
# hh
hourstr = timestr
elif len(timestr) == 4 or len(timestr) == 5:
# hh:mm
# hhmm
if timestr.count(":") == 1:
hourstr, minutestr = timestr.split(":")
else:
hourstr = timestr[0:2]
minutestr = timestr[2:]
elif len(timestr) == 6 or len(timestr) == 8:
# hh:mm:ss
# hhmmss
if timestr.count(":") == 2:
hourstr, minutestr, secondstr = timestr.split(":")
else:
hourstr = timestr[0:2]
minutestr = timestr[2:4]
secondstr = timestr[4:]
else:
raise ISOFormatError('"{0}" is not a valid ISO 8601 time.'.format(isotimestr))
for componentstr in [hourstr, minutestr, secondstr]:
if componentstr is not None and componentstr.isdigit() is False:
raise ISOFormatError(
'"{0}" is not a valid ISO 8601 time.'.format(isotimestr)
)
if fractionalstr is not None:
if secondstr is not None:
secondstr = secondstr + "." + fractionalstr
elif minutestr is not None:
minutestr = minutestr + "." + fractionalstr
else:
hourstr = hourstr + "." + fractionalstr
if tzstr is None:
tz = None
else:
tz = parse_timezone(tzstr, builder=TupleBuilder)
return builder.build_time(hh=hourstr, mm=minutestr, ss=secondstr, tz=tz)
def parse_datetime(isodatetimestr, delimiter="T", builder=PythonTimeBuilder):
# Given a string in ISO 8601 date time format, return a datetime.datetime
# object that corresponds to the given date time.
# By default, the ISO 8601 specified T delimiter is used to split the
# date and time (<date>T<time>). Fixed offset tzdata will be included
# if UTC offset is given in the input string.
if is_string(isodatetimestr) is False:
raise ValueError("Date time must be string.")
if delimiter not in isodatetimestr:
raise ISOFormatError(
'Delimiter "{0}" is not in combined date time '
'string "{1}".'.format(delimiter, isodatetimestr)
)
isodatestr, isotimestr = isodatetimestr.split(delimiter, 1)
datepart = parse_date(isodatestr, builder=TupleBuilder)
timepart = parse_time(isotimestr, builder=TupleBuilder)
return builder.build_datetime(datepart, timepart)

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
from aniso8601.builders.python import PythonTimeBuilder
from aniso8601.compat import is_string
from aniso8601.exceptions import ISOFormatError
def parse_timezone(tzstr, builder=PythonTimeBuilder):
# tzstr can be Z, ±hh:mm, ±hhmm, ±hh
if is_string(tzstr) is False:
raise ValueError("Time zone must be string.")
if len(tzstr) == 1 and tzstr[0] == "Z":
return builder.build_timezone(negative=False, Z=True, name=tzstr)
elif len(tzstr) == 6:
# ±hh:mm
hourstr = tzstr[1:3]
minutestr = tzstr[4:6]
if tzstr[0] == "-" and hourstr == "00" and minutestr == "00":
raise ISOFormatError("Negative ISO 8601 time offset must not " "be 0.")
elif len(tzstr) == 5:
# ±hhmm
hourstr = tzstr[1:3]
minutestr = tzstr[3:5]
if tzstr[0] == "-" and hourstr == "00" and minutestr == "00":
raise ISOFormatError("Negative ISO 8601 time offset must not " "be 0.")
elif len(tzstr) == 3:
# ±hh
hourstr = tzstr[1:3]
minutestr = None
if tzstr[0] == "-" and hourstr == "00":
raise ISOFormatError("Negative ISO 8601 time offset must not " "be 0.")
else:
raise ISOFormatError('"{0}" is not a valid ISO 8601 time offset.'.format(tzstr))
for componentstr in [hourstr, minutestr]:
if componentstr is not None:
if componentstr.isdigit() is False:
raise ISOFormatError(
'"{0}" is not a valid ISO 8601 time offset.'.format(tzstr)
)
if tzstr[0] == "+":
return builder.build_timezone(
negative=False, hh=hourstr, mm=minutestr, name=tzstr
)
if tzstr[0] == "-":
return builder.build_timezone(
negative=True, hh=hourstr, mm=minutestr, name=tzstr
)
raise ISOFormatError('"{0}" is not a valid ISO 8601 time offset.'.format(tzstr))

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license. See the LICENSE file for details.
import datetime
class UTCOffset(datetime.tzinfo):
def __init__(self, name=None, minutes=None):
# We build an offset in this manner since the
# tzinfo class must have an init
# "method that can be called with no arguments"
self._name = name
if minutes is not None:
self._utcdelta = datetime.timedelta(minutes=minutes)
else:
self._utcdelta = None
def __repr__(self):
if self._utcdelta >= datetime.timedelta(hours=0):
return "+{0} UTC".format(self._utcdelta)
# From the docs:
# String representations of timedelta objects are normalized
# similarly to their internal representation. This leads to
# somewhat unusual results for negative timedeltas.
# Clean this up for printing purposes
# Negative deltas start at -1 day
correcteddays = abs(self._utcdelta.days + 1)
# Negative deltas have a positive seconds
deltaseconds = (24 * 60 * 60) - self._utcdelta.seconds
# (24 hours / day) * (60 minutes / hour) * (60 seconds / hour)
days, remainder = divmod(deltaseconds, 24 * 60 * 60)
# (1 hour) * (60 minutes / hour) * (60 seconds / hour)
hours, remainder = divmod(remainder, 1 * 60 * 60)
# (1 minute) * (60 seconds / minute)
minutes, seconds = divmod(remainder, 1 * 60)
# Add any remaining days to the correcteddays count
correcteddays += days
if correcteddays == 0:
return "-{0}:{1:02}:{2:02} UTC".format(hours, minutes, seconds)
elif correcteddays == 1:
return "-1 day, {0}:{1:02}:{2:02} UTC".format(hours, minutes, seconds)
return "-{0} days, {1}:{2:02}:{3:02} UTC".format(
correcteddays, hours, minutes, seconds
)
def utcoffset(self, dt):
return self._utcdelta
def tzname(self, dt):
return self._name
def dst(self, dt):
# ISO 8601 specifies offsets should be different if DST is required,
# instead of allowing for a DST to be specified
# https://docs.python.org/2/library/datetime.html#datetime.tzinfo.dst
return datetime.timedelta(0)

@ -0,0 +1,79 @@
# SPDX-License-Identifier: MIT
import sys
from functools import partial
from . import converters, exceptions, filters, setters, validators
from ._cmp import cmp_using
from ._config import get_run_validators, set_run_validators
from ._funcs import asdict, assoc, astuple, evolve, has, resolve_types
from ._make import (
NOTHING,
Attribute,
Factory,
attrib,
attrs,
fields,
fields_dict,
make_class,
validate,
)
from ._version_info import VersionInfo
__version__ = "22.1.0"
__version_info__ = VersionInfo._from_version_string(__version__)
__title__ = "attrs"
__description__ = "Classes Without Boilerplate"
__url__ = "https://www.attrs.org/"
__uri__ = __url__
__doc__ = __description__ + " <" + __uri__ + ">"
__author__ = "Hynek Schlawack"
__email__ = "hs@ox.cx"
__license__ = "MIT"
__copyright__ = "Copyright (c) 2015 Hynek Schlawack"
s = attributes = attrs
ib = attr = attrib
dataclass = partial(attrs, auto_attribs=True) # happy Easter ;)
__all__ = [
"Attribute",
"Factory",
"NOTHING",
"asdict",
"assoc",
"astuple",
"attr",
"attrib",
"attributes",
"attrs",
"cmp_using",
"converters",
"evolve",
"exceptions",
"fields",
"fields_dict",
"filters",
"get_run_validators",
"has",
"ib",
"make_class",
"resolve_types",
"s",
"set_run_validators",
"setters",
"validate",
"validators",
]
if sys.version_info[:2] >= (3, 6):
from ._next_gen import define, field, frozen, mutable # noqa: F401
__all__.extend(("define", "field", "frozen", "mutable"))

@ -0,0 +1,486 @@
import sys
from typing import (
Any,
Callable,
ClassVar,
Dict,
Generic,
List,
Mapping,
Optional,
Protocol,
Sequence,
Tuple,
Type,
TypeVar,
Union,
overload,
)
# `import X as X` is required to make these public
from . import converters as converters
from . import exceptions as exceptions
from . import filters as filters
from . import setters as setters
from . import validators as validators
from ._cmp import cmp_using as cmp_using
from ._version_info import VersionInfo
__version__: str
__version_info__: VersionInfo
__title__: str
__description__: str
__url__: str
__uri__: str
__author__: str
__email__: str
__license__: str
__copyright__: str
_T = TypeVar("_T")
_C = TypeVar("_C", bound=type)
_EqOrderType = Union[bool, Callable[[Any], Any]]
_ValidatorType = Callable[[Any, Attribute[_T], _T], Any]
_ConverterType = Callable[[Any], Any]
_FilterType = Callable[[Attribute[_T], _T], bool]
_ReprType = Callable[[Any], str]
_ReprArgType = Union[bool, _ReprType]
_OnSetAttrType = Callable[[Any, Attribute[Any], Any], Any]
_OnSetAttrArgType = Union[
_OnSetAttrType, List[_OnSetAttrType], setters._NoOpType
]
_FieldTransformer = Callable[
[type, List[Attribute[Any]]], List[Attribute[Any]]
]
# FIXME: in reality, if multiple validators are passed they must be in a list
# or tuple, but those are invariant and so would prevent subtypes of
# _ValidatorType from working when passed in a list or tuple.
_ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]]
# A protocol to be able to statically accept an attrs class.
class AttrsInstance(Protocol):
__attrs_attrs__: ClassVar[Any]
# _make --
NOTHING: object
# NOTE: Factory lies about its return type to make this possible:
# `x: List[int] # = Factory(list)`
# Work around mypy issue #4554 in the common case by using an overload.
if sys.version_info >= (3, 8):
from typing import Literal
@overload
def Factory(factory: Callable[[], _T]) -> _T: ...
@overload
def Factory(
factory: Callable[[Any], _T],
takes_self: Literal[True],
) -> _T: ...
@overload
def Factory(
factory: Callable[[], _T],
takes_self: Literal[False],
) -> _T: ...
else:
@overload
def Factory(factory: Callable[[], _T]) -> _T: ...
@overload
def Factory(
factory: Union[Callable[[Any], _T], Callable[[], _T]],
takes_self: bool = ...,
) -> _T: ...
# Static type inference support via __dataclass_transform__ implemented as per:
# https://github.com/microsoft/pyright/blob/1.1.135/specs/dataclass_transforms.md
# This annotation must be applied to all overloads of "define" and "attrs"
#
# NOTE: This is a typing construct and does not exist at runtime. Extensions
# wrapping attrs decorators should declare a separate __dataclass_transform__
# signature in the extension module using the specification linked above to
# provide pyright support.
def __dataclass_transform__(
*,
eq_default: bool = True,
order_default: bool = False,
kw_only_default: bool = False,
field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()),
) -> Callable[[_T], _T]: ...
class Attribute(Generic[_T]):
name: str
default: Optional[_T]
validator: Optional[_ValidatorType[_T]]
repr: _ReprArgType
cmp: _EqOrderType
eq: _EqOrderType
order: _EqOrderType
hash: Optional[bool]
init: bool
converter: Optional[_ConverterType]
metadata: Dict[Any, Any]
type: Optional[Type[_T]]
kw_only: bool
on_setattr: _OnSetAttrType
def evolve(self, **changes: Any) -> "Attribute[Any]": ...
# NOTE: We had several choices for the annotation to use for type arg:
# 1) Type[_T]
# - Pros: Handles simple cases correctly
# - Cons: Might produce less informative errors in the case of conflicting
# TypeVars e.g. `attr.ib(default='bad', type=int)`
# 2) Callable[..., _T]
# - Pros: Better error messages than #1 for conflicting TypeVars
# - Cons: Terrible error messages for validator checks.
# e.g. attr.ib(type=int, validator=validate_str)
# -> error: Cannot infer function type argument
# 3) type (and do all of the work in the mypy plugin)
# - Pros: Simple here, and we could customize the plugin with our own errors.
# - Cons: Would need to write mypy plugin code to handle all the cases.
# We chose option #1.
# `attr` lies about its return type to make the following possible:
# attr() -> Any
# attr(8) -> int
# attr(validator=<some callable>) -> Whatever the callable expects.
# This makes this type of assignments possible:
# x: int = attr(8)
#
# This form catches explicit None or no default but with no other arguments
# returns Any.
@overload
def attrib(
default: None = ...,
validator: None = ...,
repr: _ReprArgType = ...,
cmp: Optional[_EqOrderType] = ...,
hash: Optional[bool] = ...,
init: bool = ...,
metadata: Optional[Mapping[Any, Any]] = ...,
type: None = ...,
converter: None = ...,
factory: None = ...,
kw_only: bool = ...,
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
) -> Any: ...
# This form catches an explicit None or no default and infers the type from the
# other arguments.
@overload
def attrib(
default: None = ...,
validator: Optional[_ValidatorArgType[_T]] = ...,
repr: _ReprArgType = ...,
cmp: Optional[_EqOrderType] = ...,
hash: Optional[bool] = ...,
init: bool = ...,
metadata: Optional[Mapping[Any, Any]] = ...,
type: Optional[Type[_T]] = ...,
converter: Optional[_ConverterType] = ...,
factory: Optional[Callable[[], _T]] = ...,
kw_only: bool = ...,
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
) -> _T: ...
# This form catches an explicit default argument.
@overload
def attrib(
default: _T,
validator: Optional[_ValidatorArgType[_T]] = ...,
repr: _ReprArgType = ...,
cmp: Optional[_EqOrderType] = ...,
hash: Optional[bool] = ...,
init: bool = ...,
metadata: Optional[Mapping[Any, Any]] = ...,
type: Optional[Type[_T]] = ...,
converter: Optional[_ConverterType] = ...,
factory: Optional[Callable[[], _T]] = ...,
kw_only: bool = ...,
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
) -> _T: ...
# This form covers type=non-Type: e.g. forward references (str), Any
@overload
def attrib(
default: Optional[_T] = ...,
validator: Optional[_ValidatorArgType[_T]] = ...,
repr: _ReprArgType = ...,
cmp: Optional[_EqOrderType] = ...,
hash: Optional[bool] = ...,
init: bool = ...,
metadata: Optional[Mapping[Any, Any]] = ...,
type: object = ...,
converter: Optional[_ConverterType] = ...,
factory: Optional[Callable[[], _T]] = ...,
kw_only: bool = ...,
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
) -> Any: ...
@overload
def field(
*,
default: None = ...,
validator: None = ...,
repr: _ReprArgType = ...,
hash: Optional[bool] = ...,
init: bool = ...,
metadata: Optional[Mapping[Any, Any]] = ...,
converter: None = ...,
factory: None = ...,
kw_only: bool = ...,
eq: Optional[bool] = ...,
order: Optional[bool] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
) -> Any: ...
# This form catches an explicit None or no default and infers the type from the
# other arguments.
@overload
def field(
*,
default: None = ...,
validator: Optional[_ValidatorArgType[_T]] = ...,
repr: _ReprArgType = ...,
hash: Optional[bool] = ...,
init: bool = ...,
metadata: Optional[Mapping[Any, Any]] = ...,
converter: Optional[_ConverterType] = ...,
factory: Optional[Callable[[], _T]] = ...,
kw_only: bool = ...,
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
) -> _T: ...
# This form catches an explicit default argument.
@overload
def field(
*,
default: _T,
validator: Optional[_ValidatorArgType[_T]] = ...,
repr: _ReprArgType = ...,
hash: Optional[bool] = ...,
init: bool = ...,
metadata: Optional[Mapping[Any, Any]] = ...,
converter: Optional[_ConverterType] = ...,
factory: Optional[Callable[[], _T]] = ...,
kw_only: bool = ...,
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
) -> _T: ...
# This form covers type=non-Type: e.g. forward references (str), Any
@overload
def field(
*,
default: Optional[_T] = ...,
validator: Optional[_ValidatorArgType[_T]] = ...,
repr: _ReprArgType = ...,
hash: Optional[bool] = ...,
init: bool = ...,
metadata: Optional[Mapping[Any, Any]] = ...,
converter: Optional[_ConverterType] = ...,
factory: Optional[Callable[[], _T]] = ...,
kw_only: bool = ...,
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
) -> Any: ...
@overload
@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field))
def attrs(
maybe_cls: _C,
these: Optional[Dict[str, Any]] = ...,
repr_ns: Optional[str] = ...,
repr: bool = ...,
cmp: Optional[_EqOrderType] = ...,
hash: Optional[bool] = ...,
init: bool = ...,
slots: bool = ...,
frozen: bool = ...,
weakref_slot: bool = ...,
str: bool = ...,
auto_attribs: bool = ...,
kw_only: bool = ...,
cache_hash: bool = ...,
auto_exc: bool = ...,
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
auto_detect: bool = ...,
collect_by_mro: bool = ...,
getstate_setstate: Optional[bool] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
field_transformer: Optional[_FieldTransformer] = ...,
match_args: bool = ...,
) -> _C: ...
@overload
@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field))
def attrs(
maybe_cls: None = ...,
these: Optional[Dict[str, Any]] = ...,
repr_ns: Optional[str] = ...,
repr: bool = ...,
cmp: Optional[_EqOrderType] = ...,
hash: Optional[bool] = ...,
init: bool = ...,
slots: bool = ...,
frozen: bool = ...,
weakref_slot: bool = ...,
str: bool = ...,
auto_attribs: bool = ...,
kw_only: bool = ...,
cache_hash: bool = ...,
auto_exc: bool = ...,
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
auto_detect: bool = ...,
collect_by_mro: bool = ...,
getstate_setstate: Optional[bool] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
field_transformer: Optional[_FieldTransformer] = ...,
match_args: bool = ...,
) -> Callable[[_C], _C]: ...
@overload
@__dataclass_transform__(field_descriptors=(attrib, field))
def define(
maybe_cls: _C,
*,
these: Optional[Dict[str, Any]] = ...,
repr: bool = ...,
hash: Optional[bool] = ...,
init: bool = ...,
slots: bool = ...,
frozen: bool = ...,
weakref_slot: bool = ...,
str: bool = ...,
auto_attribs: bool = ...,
kw_only: bool = ...,
cache_hash: bool = ...,
auto_exc: bool = ...,
eq: Optional[bool] = ...,
order: Optional[bool] = ...,
auto_detect: bool = ...,
getstate_setstate: Optional[bool] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
field_transformer: Optional[_FieldTransformer] = ...,
match_args: bool = ...,
) -> _C: ...
@overload
@__dataclass_transform__(field_descriptors=(attrib, field))
def define(
maybe_cls: None = ...,
*,
these: Optional[Dict[str, Any]] = ...,
repr: bool = ...,
hash: Optional[bool] = ...,
init: bool = ...,
slots: bool = ...,
frozen: bool = ...,
weakref_slot: bool = ...,
str: bool = ...,
auto_attribs: bool = ...,
kw_only: bool = ...,
cache_hash: bool = ...,
auto_exc: bool = ...,
eq: Optional[bool] = ...,
order: Optional[bool] = ...,
auto_detect: bool = ...,
getstate_setstate: Optional[bool] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
field_transformer: Optional[_FieldTransformer] = ...,
match_args: bool = ...,
) -> Callable[[_C], _C]: ...
mutable = define
frozen = define # they differ only in their defaults
def fields(cls: Type[AttrsInstance]) -> Any: ...
def fields_dict(cls: Type[AttrsInstance]) -> Dict[str, Attribute[Any]]: ...
def validate(inst: AttrsInstance) -> None: ...
def resolve_types(
cls: _C,
globalns: Optional[Dict[str, Any]] = ...,
localns: Optional[Dict[str, Any]] = ...,
attribs: Optional[List[Attribute[Any]]] = ...,
) -> _C: ...
# TODO: add support for returning a proper attrs class from the mypy plugin
# we use Any instead of _CountingAttr so that e.g. `make_class('Foo',
# [attr.ib()])` is valid
def make_class(
name: str,
attrs: Union[List[str], Tuple[str, ...], Dict[str, Any]],
bases: Tuple[type, ...] = ...,
repr_ns: Optional[str] = ...,
repr: bool = ...,
cmp: Optional[_EqOrderType] = ...,
hash: Optional[bool] = ...,
init: bool = ...,
slots: bool = ...,
frozen: bool = ...,
weakref_slot: bool = ...,
str: bool = ...,
auto_attribs: bool = ...,
kw_only: bool = ...,
cache_hash: bool = ...,
auto_exc: bool = ...,
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
collect_by_mro: bool = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
field_transformer: Optional[_FieldTransformer] = ...,
) -> type: ...
# _funcs --
# TODO: add support for returning TypedDict from the mypy plugin
# FIXME: asdict/astuple do not honor their factory args. Waiting on one of
# these:
# https://github.com/python/mypy/issues/4236
# https://github.com/python/typing/issues/253
# XXX: remember to fix attrs.asdict/astuple too!
def asdict(
inst: AttrsInstance,
recurse: bool = ...,
filter: Optional[_FilterType[Any]] = ...,
dict_factory: Type[Mapping[Any, Any]] = ...,
retain_collection_types: bool = ...,
value_serializer: Optional[
Callable[[type, Attribute[Any], Any], Any]
] = ...,
tuple_keys: Optional[bool] = ...,
) -> Dict[str, Any]: ...
# TODO: add support for returning NamedTuple from the mypy plugin
def astuple(
inst: AttrsInstance,
recurse: bool = ...,
filter: Optional[_FilterType[Any]] = ...,
tuple_factory: Type[Sequence[Any]] = ...,
retain_collection_types: bool = ...,
) -> Tuple[Any, ...]: ...
def has(cls: type) -> bool: ...
def assoc(inst: _T, **changes: Any) -> _T: ...
def evolve(inst: _T, **changes: Any) -> _T: ...
# _config --
def set_run_validators(run: bool) -> None: ...
def get_run_validators() -> bool: ...
# aliases --
s = attributes = attrs
ib = attr = attrib
dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;)

@ -0,0 +1,155 @@
# SPDX-License-Identifier: MIT
import functools
import types
from ._make import _make_ne
_operation_names = {"eq": "==", "lt": "<", "le": "<=", "gt": ">", "ge": ">="}
def cmp_using(
eq=None,
lt=None,
le=None,
gt=None,
ge=None,
require_same_type=True,
class_name="Comparable",
):
"""
Create a class that can be passed into `attr.ib`'s ``eq``, ``order``, and
``cmp`` arguments to customize field comparison.
The resulting class will have a full set of ordering methods if
at least one of ``{lt, le, gt, ge}`` and ``eq`` are provided.
:param Optional[callable] eq: `callable` used to evaluate equality
of two objects.
:param Optional[callable] lt: `callable` used to evaluate whether
one object is less than another object.
:param Optional[callable] le: `callable` used to evaluate whether
one object is less than or equal to another object.
:param Optional[callable] gt: `callable` used to evaluate whether
one object is greater than another object.
:param Optional[callable] ge: `callable` used to evaluate whether
one object is greater than or equal to another object.
:param bool require_same_type: When `True`, equality and ordering methods
will return `NotImplemented` if objects are not of the same type.
:param Optional[str] class_name: Name of class. Defaults to 'Comparable'.
See `comparison` for more details.
.. versionadded:: 21.1.0
"""
body = {
"__slots__": ["value"],
"__init__": _make_init(),
"_requirements": [],
"_is_comparable_to": _is_comparable_to,
}
# Add operations.
num_order_functions = 0
has_eq_function = False
if eq is not None:
has_eq_function = True
body["__eq__"] = _make_operator("eq", eq)
body["__ne__"] = _make_ne()
if lt is not None:
num_order_functions += 1
body["__lt__"] = _make_operator("lt", lt)
if le is not None:
num_order_functions += 1
body["__le__"] = _make_operator("le", le)
if gt is not None:
num_order_functions += 1
body["__gt__"] = _make_operator("gt", gt)
if ge is not None:
num_order_functions += 1
body["__ge__"] = _make_operator("ge", ge)
type_ = types.new_class(
class_name, (object,), {}, lambda ns: ns.update(body)
)
# Add same type requirement.
if require_same_type:
type_._requirements.append(_check_same_type)
# Add total ordering if at least one operation was defined.
if 0 < num_order_functions < 4:
if not has_eq_function:
# functools.total_ordering requires __eq__ to be defined,
# so raise early error here to keep a nice stack.
raise ValueError(
"eq must be define is order to complete ordering from "
"lt, le, gt, ge."
)
type_ = functools.total_ordering(type_)
return type_
def _make_init():
"""
Create __init__ method.
"""
def __init__(self, value):
"""
Initialize object with *value*.
"""
self.value = value
return __init__
def _make_operator(name, func):
"""
Create operator method.
"""
def method(self, other):
if not self._is_comparable_to(other):
return NotImplemented
result = func(self.value, other.value)
if result is NotImplemented:
return NotImplemented
return result
method.__name__ = "__%s__" % (name,)
method.__doc__ = "Return a %s b. Computed by attrs." % (
_operation_names[name],
)
return method
def _is_comparable_to(self, other):
"""
Check whether `other` is comparable to `self`.
"""
for func in self._requirements:
if not func(self, other):
return False
return True
def _check_same_type(self, other):
"""
Return True if *self* and *other* are of the same type, False otherwise.
"""
return other.value.__class__ is self.value.__class__

@ -0,0 +1,13 @@
from typing import Any, Callable, Optional, Type
_CompareWithType = Callable[[Any, Any], bool]
def cmp_using(
eq: Optional[_CompareWithType],
lt: Optional[_CompareWithType],
le: Optional[_CompareWithType],
gt: Optional[_CompareWithType],
ge: Optional[_CompareWithType],
require_same_type: bool,
class_name: str,
) -> Type: ...

@ -0,0 +1,185 @@
# SPDX-License-Identifier: MIT
import inspect
import platform
import sys
import threading
import types
import warnings
from collections.abc import Mapping, Sequence # noqa
PYPY = platform.python_implementation() == "PyPy"
PY36 = sys.version_info[:2] >= (3, 6)
HAS_F_STRINGS = PY36
PY310 = sys.version_info[:2] >= (3, 10)
if PYPY or PY36:
ordered_dict = dict
else:
from collections import OrderedDict
ordered_dict = OrderedDict
def just_warn(*args, **kw):
warnings.warn(
"Running interpreter doesn't sufficiently support code object "
"introspection. Some features like bare super() or accessing "
"__class__ will not work with slotted classes.",
RuntimeWarning,
stacklevel=2,
)
class _AnnotationExtractor:
"""
Extract type annotations from a callable, returning None whenever there
is none.
"""
__slots__ = ["sig"]
def __init__(self, callable):
try:
self.sig = inspect.signature(callable)
except (ValueError, TypeError): # inspect failed
self.sig = None
def get_first_param_type(self):
"""
Return the type annotation of the first argument if it's not empty.
"""
if not self.sig:
return None
params = list(self.sig.parameters.values())
if params and params[0].annotation is not inspect.Parameter.empty:
return params[0].annotation
return None
def get_return_type(self):
"""
Return the return type if it's not empty.
"""
if (
self.sig
and self.sig.return_annotation is not inspect.Signature.empty
):
return self.sig.return_annotation
return None
def make_set_closure_cell():
"""Return a function of two arguments (cell, value) which sets
the value stored in the closure cell `cell` to `value`.
"""
# pypy makes this easy. (It also supports the logic below, but
# why not do the easy/fast thing?)
if PYPY:
def set_closure_cell(cell, value):
cell.__setstate__((value,))
return set_closure_cell
# Otherwise gotta do it the hard way.
# Create a function that will set its first cellvar to `value`.
def set_first_cellvar_to(value):
x = value
return
# This function will be eliminated as dead code, but
# not before its reference to `x` forces `x` to be
# represented as a closure cell rather than a local.
def force_x_to_be_a_cell(): # pragma: no cover
return x
try:
# Extract the code object and make sure our assumptions about
# the closure behavior are correct.
co = set_first_cellvar_to.__code__
if co.co_cellvars != ("x",) or co.co_freevars != ():
raise AssertionError # pragma: no cover
# Convert this code object to a code object that sets the
# function's first _freevar_ (not cellvar) to the argument.
if sys.version_info >= (3, 8):
def set_closure_cell(cell, value):
cell.cell_contents = value
else:
args = [co.co_argcount]
args.append(co.co_kwonlyargcount)
args.extend(
[
co.co_nlocals,
co.co_stacksize,
co.co_flags,
co.co_code,
co.co_consts,
co.co_names,
co.co_varnames,
co.co_filename,
co.co_name,
co.co_firstlineno,
co.co_lnotab,
# These two arguments are reversed:
co.co_cellvars,
co.co_freevars,
]
)
set_first_freevar_code = types.CodeType(*args)
def set_closure_cell(cell, value):
# Create a function using the set_first_freevar_code,
# whose first closure cell is `cell`. Calling it will
# change the value of that cell.
setter = types.FunctionType(
set_first_freevar_code, {}, "setter", (), (cell,)
)
# And call it to set the cell.
setter(value)
# Make sure it works on this interpreter:
def make_func_with_cell():
x = None
def func():
return x # pragma: no cover
return func
cell = make_func_with_cell().__closure__[0]
set_closure_cell(cell, 100)
if cell.cell_contents != 100:
raise AssertionError # pragma: no cover
except Exception:
return just_warn
else:
return set_closure_cell
set_closure_cell = make_set_closure_cell()
# Thread-local global to track attrs instances which are already being repr'd.
# This is needed because there is no other (thread-safe) way to pass info
# about the instances that are already being repr'd through the call stack
# in order to ensure we don't perform infinite recursion.
#
# For instance, if an instance contains a dict which contains that instance,
# we need to know that we're already repr'ing the outside instance from within
# the dict's repr() call.
#
# This lives here rather than in _make.py so that the functions in _make.py
# don't have a direct reference to the thread-local in their globals dict.
# If they have such a reference, it breaks cloudpickle.
repr_context = threading.local()

@ -0,0 +1,31 @@
# SPDX-License-Identifier: MIT
__all__ = ["set_run_validators", "get_run_validators"]
_run_validators = True
def set_run_validators(run):
"""
Set whether or not validators are run. By default, they are run.
.. deprecated:: 21.3.0 It will not be removed, but it also will not be
moved to new ``attrs`` namespace. Use `attrs.validators.set_disabled()`
instead.
"""
if not isinstance(run, bool):
raise TypeError("'run' must be bool.")
global _run_validators
_run_validators = run
def get_run_validators():
"""
Return whether or not validators are run.
.. deprecated:: 21.3.0 It will not be removed, but it also will not be
moved to new ``attrs`` namespace. Use `attrs.validators.get_disabled()`
instead.
"""
return _run_validators

@ -0,0 +1,420 @@
# SPDX-License-Identifier: MIT
import copy
from ._make import NOTHING, _obj_setattr, fields
from .exceptions import AttrsAttributeNotFoundError
def asdict(
inst,
recurse=True,
filter=None,
dict_factory=dict,
retain_collection_types=False,
value_serializer=None,
):
"""
Return the ``attrs`` attribute values of *inst* as a dict.
Optionally recurse into other ``attrs``-decorated classes.
:param inst: Instance of an ``attrs``-decorated class.
:param bool recurse: Recurse into classes that are also
``attrs``-decorated.
:param callable filter: A callable whose return code determines whether an
attribute or element is included (``True``) or dropped (``False``). Is
called with the `attrs.Attribute` as the first argument and the
value as the second argument.
:param callable dict_factory: A callable to produce dictionaries from. For
example, to produce ordered dictionaries instead of normal Python
dictionaries, pass in ``collections.OrderedDict``.
:param bool retain_collection_types: Do not convert to ``list`` when
encountering an attribute whose type is ``tuple`` or ``set``. Only
meaningful if ``recurse`` is ``True``.
:param Optional[callable] value_serializer: A hook that is called for every
attribute or dict key/value. It receives the current instance, field
and value and must return the (updated) value. The hook is run *after*
the optional *filter* has been applied.
:rtype: return type of *dict_factory*
:raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
class.
.. versionadded:: 16.0.0 *dict_factory*
.. versionadded:: 16.1.0 *retain_collection_types*
.. versionadded:: 20.3.0 *value_serializer*
.. versionadded:: 21.3.0 If a dict has a collection for a key, it is
serialized as a tuple.
"""
attrs = fields(inst.__class__)
rv = dict_factory()
for a in attrs:
v = getattr(inst, a.name)
if filter is not None and not filter(a, v):
continue
if value_serializer is not None:
v = value_serializer(inst, a, v)
if recurse is True:
if has(v.__class__):
rv[a.name] = asdict(
v,
recurse=True,
filter=filter,
dict_factory=dict_factory,
retain_collection_types=retain_collection_types,
value_serializer=value_serializer,
)
elif isinstance(v, (tuple, list, set, frozenset)):
cf = v.__class__ if retain_collection_types is True else list
rv[a.name] = cf(
[
_asdict_anything(
i,
is_key=False,
filter=filter,
dict_factory=dict_factory,
retain_collection_types=retain_collection_types,
value_serializer=value_serializer,
)
for i in v
]
)
elif isinstance(v, dict):
df = dict_factory
rv[a.name] = df(
(
_asdict_anything(
kk,
is_key=True,
filter=filter,
dict_factory=df,
retain_collection_types=retain_collection_types,
value_serializer=value_serializer,
),
_asdict_anything(
vv,
is_key=False,
filter=filter,
dict_factory=df,
retain_collection_types=retain_collection_types,
value_serializer=value_serializer,
),
)
for kk, vv in v.items()
)
else:
rv[a.name] = v
else:
rv[a.name] = v
return rv
def _asdict_anything(
val,
is_key,
filter,
dict_factory,
retain_collection_types,
value_serializer,
):
"""
``asdict`` only works on attrs instances, this works on anything.
"""
if getattr(val.__class__, "__attrs_attrs__", None) is not None:
# Attrs class.
rv = asdict(
val,
recurse=True,
filter=filter,
dict_factory=dict_factory,
retain_collection_types=retain_collection_types,
value_serializer=value_serializer,
)
elif isinstance(val, (tuple, list, set, frozenset)):
if retain_collection_types is True:
cf = val.__class__
elif is_key:
cf = tuple
else:
cf = list
rv = cf(
[
_asdict_anything(
i,
is_key=False,
filter=filter,
dict_factory=dict_factory,
retain_collection_types=retain_collection_types,
value_serializer=value_serializer,
)
for i in val
]
)
elif isinstance(val, dict):
df = dict_factory
rv = df(
(
_asdict_anything(
kk,
is_key=True,
filter=filter,
dict_factory=df,
retain_collection_types=retain_collection_types,
value_serializer=value_serializer,
),
_asdict_anything(
vv,
is_key=False,
filter=filter,
dict_factory=df,
retain_collection_types=retain_collection_types,
value_serializer=value_serializer,
),
)
for kk, vv in val.items()
)
else:
rv = val
if value_serializer is not None:
rv = value_serializer(None, None, rv)
return rv
def astuple(
inst,
recurse=True,
filter=None,
tuple_factory=tuple,
retain_collection_types=False,
):
"""
Return the ``attrs`` attribute values of *inst* as a tuple.
Optionally recurse into other ``attrs``-decorated classes.
:param inst: Instance of an ``attrs``-decorated class.
:param bool recurse: Recurse into classes that are also
``attrs``-decorated.
:param callable filter: A callable whose return code determines whether an
attribute or element is included (``True``) or dropped (``False``). Is
called with the `attrs.Attribute` as the first argument and the
value as the second argument.
:param callable tuple_factory: A callable to produce tuples from. For
example, to produce lists instead of tuples.
:param bool retain_collection_types: Do not convert to ``list``
or ``dict`` when encountering an attribute which type is
``tuple``, ``dict`` or ``set``. Only meaningful if ``recurse`` is
``True``.
:rtype: return type of *tuple_factory*
:raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
class.
.. versionadded:: 16.2.0
"""
attrs = fields(inst.__class__)
rv = []
retain = retain_collection_types # Very long. :/
for a in attrs:
v = getattr(inst, a.name)
if filter is not None and not filter(a, v):
continue
if recurse is True:
if has(v.__class__):
rv.append(
astuple(
v,
recurse=True,
filter=filter,
tuple_factory=tuple_factory,
retain_collection_types=retain,
)
)
elif isinstance(v, (tuple, list, set, frozenset)):
cf = v.__class__ if retain is True else list
rv.append(
cf(
[
astuple(
j,
recurse=True,
filter=filter,
tuple_factory=tuple_factory,
retain_collection_types=retain,
)
if has(j.__class__)
else j
for j in v
]
)
)
elif isinstance(v, dict):
df = v.__class__ if retain is True else dict
rv.append(
df(
(
astuple(
kk,
tuple_factory=tuple_factory,
retain_collection_types=retain,
)
if has(kk.__class__)
else kk,
astuple(
vv,
tuple_factory=tuple_factory,
retain_collection_types=retain,
)
if has(vv.__class__)
else vv,
)
for kk, vv in v.items()
)
)
else:
rv.append(v)
else:
rv.append(v)
return rv if tuple_factory is list else tuple_factory(rv)
def has(cls):
"""
Check whether *cls* is a class with ``attrs`` attributes.
:param type cls: Class to introspect.
:raise TypeError: If *cls* is not a class.
:rtype: bool
"""
return getattr(cls, "__attrs_attrs__", None) is not None
def assoc(inst, **changes):
"""
Copy *inst* and apply *changes*.
:param inst: Instance of a class with ``attrs`` attributes.
:param changes: Keyword changes in the new copy.
:return: A copy of inst with *changes* incorporated.
:raise attr.exceptions.AttrsAttributeNotFoundError: If *attr_name* couldn't
be found on *cls*.
:raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
class.
.. deprecated:: 17.1.0
Use `attrs.evolve` instead if you can.
This function will not be removed du to the slightly different approach
compared to `attrs.evolve`.
"""
import warnings
warnings.warn(
"assoc is deprecated and will be removed after 2018/01.",
DeprecationWarning,
stacklevel=2,
)
new = copy.copy(inst)
attrs = fields(inst.__class__)
for k, v in changes.items():
a = getattr(attrs, k, NOTHING)
if a is NOTHING:
raise AttrsAttributeNotFoundError(
"{k} is not an attrs attribute on {cl}.".format(
k=k, cl=new.__class__
)
)
_obj_setattr(new, k, v)
return new
def evolve(inst, **changes):
"""
Create a new instance, based on *inst* with *changes* applied.
:param inst: Instance of a class with ``attrs`` attributes.
:param changes: Keyword changes in the new copy.
:return: A copy of inst with *changes* incorporated.
:raise TypeError: If *attr_name* couldn't be found in the class
``__init__``.
:raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
class.
.. versionadded:: 17.1.0
"""
cls = inst.__class__
attrs = fields(cls)
for a in attrs:
if not a.init:
continue
attr_name = a.name # To deal with private attributes.
init_name = attr_name if attr_name[0] != "_" else attr_name[1:]
if init_name not in changes:
changes[init_name] = getattr(inst, attr_name)
return cls(**changes)
def resolve_types(cls, globalns=None, localns=None, attribs=None):
"""
Resolve any strings and forward annotations in type annotations.
This is only required if you need concrete types in `Attribute`'s *type*
field. In other words, you don't need to resolve your types if you only
use them for static type checking.
With no arguments, names will be looked up in the module in which the class
was created. If this is not what you want, e.g. if the name only exists
inside a method, you may pass *globalns* or *localns* to specify other
dictionaries in which to look up these names. See the docs of
`typing.get_type_hints` for more details.
:param type cls: Class to resolve.
:param Optional[dict] globalns: Dictionary containing global variables.
:param Optional[dict] localns: Dictionary containing local variables.
:param Optional[list] attribs: List of attribs for the given class.
This is necessary when calling from inside a ``field_transformer``
since *cls* is not an ``attrs`` class yet.
:raise TypeError: If *cls* is not a class.
:raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
class and you didn't pass any attribs.
:raise NameError: If types cannot be resolved because of missing variables.
:returns: *cls* so you can use this function also as a class decorator.
Please note that you have to apply it **after** `attrs.define`. That
means the decorator has to come in the line **before** `attrs.define`.
.. versionadded:: 20.1.0
.. versionadded:: 21.1.0 *attribs*
"""
# Since calling get_type_hints is expensive we cache whether we've
# done it already.
if getattr(cls, "__attrs_types_resolved__", None) != cls:
import typing
hints = typing.get_type_hints(cls, globalns=globalns, localns=localns)
for field in fields(cls) if attribs is None else attribs:
if field.name in hints:
# Since fields have been frozen we must work around it.
_obj_setattr(field, "type", hints[field.name])
# We store the class we resolved so that subclasses know they haven't
# been resolved.
cls.__attrs_types_resolved__ = cls
# Return the class so you can use it as a decorator too.
return cls

File diff suppressed because it is too large Load Diff

@ -0,0 +1,220 @@
# SPDX-License-Identifier: MIT
"""
These are Python 3.6+-only and keyword-only APIs that call `attr.s` and
`attr.ib` with different default values.
"""
from functools import partial
from . import setters
from ._funcs import asdict as _asdict
from ._funcs import astuple as _astuple
from ._make import (
NOTHING,
_frozen_setattrs,
_ng_default_on_setattr,
attrib,
attrs,
)
from .exceptions import UnannotatedAttributeError
def define(
maybe_cls=None,
*,
these=None,
repr=None,
hash=None,
init=None,
slots=True,
frozen=False,
weakref_slot=True,
str=False,
auto_attribs=None,
kw_only=False,
cache_hash=False,
auto_exc=True,
eq=None,
order=False,
auto_detect=True,
getstate_setstate=None,
on_setattr=None,
field_transformer=None,
match_args=True,
):
r"""
Define an ``attrs`` class.
Differences to the classic `attr.s` that it uses underneath:
- Automatically detect whether or not *auto_attribs* should be `True` (c.f.
*auto_attribs* parameter).
- If *frozen* is `False`, run converters and validators when setting an
attribute by default.
- *slots=True*
.. caution::
Usually this has only upsides and few visible effects in everyday
programming. But it *can* lead to some suprising behaviors, so please
make sure to read :term:`slotted classes`.
- *auto_exc=True*
- *auto_detect=True*
- *order=False*
- Some options that were only relevant on Python 2 or were kept around for
backwards-compatibility have been removed.
Please note that these are all defaults and you can change them as you
wish.
:param Optional[bool] auto_attribs: If set to `True` or `False`, it behaves
exactly like `attr.s`. If left `None`, `attr.s` will try to guess:
1. If any attributes are annotated and no unannotated `attrs.fields`\ s
are found, it assumes *auto_attribs=True*.
2. Otherwise it assumes *auto_attribs=False* and tries to collect
`attrs.fields`\ s.
For now, please refer to `attr.s` for the rest of the parameters.
.. versionadded:: 20.1.0
.. versionchanged:: 21.3.0 Converters are also run ``on_setattr``.
"""
def do_it(cls, auto_attribs):
return attrs(
maybe_cls=cls,
these=these,
repr=repr,
hash=hash,
init=init,
slots=slots,
frozen=frozen,
weakref_slot=weakref_slot,
str=str,
auto_attribs=auto_attribs,
kw_only=kw_only,
cache_hash=cache_hash,
auto_exc=auto_exc,
eq=eq,
order=order,
auto_detect=auto_detect,
collect_by_mro=True,
getstate_setstate=getstate_setstate,
on_setattr=on_setattr,
field_transformer=field_transformer,
match_args=match_args,
)
def wrap(cls):
"""
Making this a wrapper ensures this code runs during class creation.
We also ensure that frozen-ness of classes is inherited.
"""
nonlocal frozen, on_setattr
had_on_setattr = on_setattr not in (None, setters.NO_OP)
# By default, mutable classes convert & validate on setattr.
if frozen is False and on_setattr is None:
on_setattr = _ng_default_on_setattr
# However, if we subclass a frozen class, we inherit the immutability
# and disable on_setattr.
for base_cls in cls.__bases__:
if base_cls.__setattr__ is _frozen_setattrs:
if had_on_setattr:
raise ValueError(
"Frozen classes can't use on_setattr "
"(frozen-ness was inherited)."
)
on_setattr = setters.NO_OP
break
if auto_attribs is not None:
return do_it(cls, auto_attribs)
try:
return do_it(cls, True)
except UnannotatedAttributeError:
return do_it(cls, False)
# maybe_cls's type depends on the usage of the decorator. It's a class
# if it's used as `@attrs` but ``None`` if used as `@attrs()`.
if maybe_cls is None:
return wrap
else:
return wrap(maybe_cls)
mutable = define
frozen = partial(define, frozen=True, on_setattr=None)
def field(
*,
default=NOTHING,
validator=None,
repr=True,
hash=None,
init=True,
metadata=None,
converter=None,
factory=None,
kw_only=False,
eq=None,
order=None,
on_setattr=None,
):
"""
Identical to `attr.ib`, except keyword-only and with some arguments
removed.
.. versionadded:: 20.1.0
"""
return attrib(
default=default,
validator=validator,
repr=repr,
hash=hash,
init=init,
metadata=metadata,
converter=converter,
factory=factory,
kw_only=kw_only,
eq=eq,
order=order,
on_setattr=on_setattr,
)
def asdict(inst, *, recurse=True, filter=None, value_serializer=None):
"""
Same as `attr.asdict`, except that collections types are always retained
and dict is always used as *dict_factory*.
.. versionadded:: 21.3.0
"""
return _asdict(
inst=inst,
recurse=recurse,
filter=filter,
value_serializer=value_serializer,
retain_collection_types=True,
)
def astuple(inst, *, recurse=True, filter=None):
"""
Same as `attr.astuple`, except that collections types are always retained
and `tuple` is always used as the *tuple_factory*.
.. versionadded:: 21.3.0
"""
return _astuple(
inst=inst, recurse=recurse, filter=filter, retain_collection_types=True
)

@ -0,0 +1,86 @@
# SPDX-License-Identifier: MIT
from functools import total_ordering
from ._funcs import astuple
from ._make import attrib, attrs
@total_ordering
@attrs(eq=False, order=False, slots=True, frozen=True)
class VersionInfo:
"""
A version object that can be compared to tuple of length 1--4:
>>> attr.VersionInfo(19, 1, 0, "final") <= (19, 2)
True
>>> attr.VersionInfo(19, 1, 0, "final") < (19, 1, 1)
True
>>> vi = attr.VersionInfo(19, 2, 0, "final")
>>> vi < (19, 1, 1)
False
>>> vi < (19,)
False
>>> vi == (19, 2,)
True
>>> vi == (19, 2, 1)
False
.. versionadded:: 19.2
"""
year = attrib(type=int)
minor = attrib(type=int)
micro = attrib(type=int)
releaselevel = attrib(type=str)
@classmethod
def _from_version_string(cls, s):
"""
Parse *s* and return a _VersionInfo.
"""
v = s.split(".")
if len(v) == 3:
v.append("final")
return cls(
year=int(v[0]), minor=int(v[1]), micro=int(v[2]), releaselevel=v[3]
)
def _ensure_tuple(self, other):
"""
Ensure *other* is a tuple of a valid length.
Returns a possibly transformed *other* and ourselves as a tuple of
the same length as *other*.
"""
if self.__class__ is other.__class__:
other = astuple(other)
if not isinstance(other, tuple):
raise NotImplementedError
if not (1 <= len(other) <= 4):
raise NotImplementedError
return astuple(self)[: len(other)], other
def __eq__(self, other):
try:
us, them = self._ensure_tuple(other)
except NotImplementedError:
return NotImplemented
return us == them
def __lt__(self, other):
try:
us, them = self._ensure_tuple(other)
except NotImplementedError:
return NotImplemented
# Since alphabetically "dev0" < "final" < "post1" < "post2", we don't
# have to do anything special with releaselevel for now.
return us < them

@ -0,0 +1,9 @@
class VersionInfo:
@property
def year(self) -> int: ...
@property
def minor(self) -> int: ...
@property
def micro(self) -> int: ...
@property
def releaselevel(self) -> str: ...

@ -0,0 +1,144 @@
# SPDX-License-Identifier: MIT
"""
Commonly useful converters.
"""
import typing
from ._compat import _AnnotationExtractor
from ._make import NOTHING, Factory, pipe
__all__ = [
"default_if_none",
"optional",
"pipe",
"to_bool",
]
def optional(converter):
"""
A converter that allows an attribute to be optional. An optional attribute
is one which can be set to ``None``.
Type annotations will be inferred from the wrapped converter's, if it
has any.
:param callable converter: the converter that is used for non-``None``
values.
.. versionadded:: 17.1.0
"""
def optional_converter(val):
if val is None:
return None
return converter(val)
xtr = _AnnotationExtractor(converter)
t = xtr.get_first_param_type()
if t:
optional_converter.__annotations__["val"] = typing.Optional[t]
rt = xtr.get_return_type()
if rt:
optional_converter.__annotations__["return"] = typing.Optional[rt]
return optional_converter
def default_if_none(default=NOTHING, factory=None):
"""
A converter that allows to replace ``None`` values by *default* or the
result of *factory*.
:param default: Value to be used if ``None`` is passed. Passing an instance
of `attrs.Factory` is supported, however the ``takes_self`` option
is *not*.
:param callable factory: A callable that takes no parameters whose result
is used if ``None`` is passed.
:raises TypeError: If **neither** *default* or *factory* is passed.
:raises TypeError: If **both** *default* and *factory* are passed.
:raises ValueError: If an instance of `attrs.Factory` is passed with
``takes_self=True``.
.. versionadded:: 18.2.0
"""
if default is NOTHING and factory is None:
raise TypeError("Must pass either `default` or `factory`.")
if default is not NOTHING and factory is not None:
raise TypeError(
"Must pass either `default` or `factory` but not both."
)
if factory is not None:
default = Factory(factory)
if isinstance(default, Factory):
if default.takes_self:
raise ValueError(
"`takes_self` is not supported by default_if_none."
)
def default_if_none_converter(val):
if val is not None:
return val
return default.factory()
else:
def default_if_none_converter(val):
if val is not None:
return val
return default
return default_if_none_converter
def to_bool(val):
"""
Convert "boolean" strings (e.g., from env. vars.) to real booleans.
Values mapping to :code:`True`:
- :code:`True`
- :code:`"true"` / :code:`"t"`
- :code:`"yes"` / :code:`"y"`
- :code:`"on"`
- :code:`"1"`
- :code:`1`
Values mapping to :code:`False`:
- :code:`False`
- :code:`"false"` / :code:`"f"`
- :code:`"no"` / :code:`"n"`
- :code:`"off"`
- :code:`"0"`
- :code:`0`
:raises ValueError: for any other value.
.. versionadded:: 21.3.0
"""
if isinstance(val, str):
val = val.lower()
truthy = {True, "true", "t", "yes", "y", "on", "1", 1}
falsy = {False, "false", "f", "no", "n", "off", "0", 0}
try:
if val in truthy:
return True
if val in falsy:
return False
except TypeError:
# Raised when "val" is not hashable (e.g., lists)
pass
raise ValueError("Cannot convert value to bool: {}".format(val))

@ -0,0 +1,13 @@
from typing import Callable, Optional, TypeVar, overload
from . import _ConverterType
_T = TypeVar("_T")
def pipe(*validators: _ConverterType) -> _ConverterType: ...
def optional(converter: _ConverterType) -> _ConverterType: ...
@overload
def default_if_none(default: _T) -> _ConverterType: ...
@overload
def default_if_none(*, factory: Callable[[], _T]) -> _ConverterType: ...
def to_bool(val: str) -> bool: ...

@ -0,0 +1,92 @@
# SPDX-License-Identifier: MIT
class FrozenError(AttributeError):
"""
A frozen/immutable instance or attribute have been attempted to be
modified.
It mirrors the behavior of ``namedtuples`` by using the same error message
and subclassing `AttributeError`.
.. versionadded:: 20.1.0
"""
msg = "can't set attribute"
args = [msg]
class FrozenInstanceError(FrozenError):
"""
A frozen instance has been attempted to be modified.
.. versionadded:: 16.1.0
"""
class FrozenAttributeError(FrozenError):
"""
A frozen attribute has been attempted to be modified.
.. versionadded:: 20.1.0
"""
class AttrsAttributeNotFoundError(ValueError):
"""
An ``attrs`` function couldn't find an attribute that the user asked for.
.. versionadded:: 16.2.0
"""
class NotAnAttrsClassError(ValueError):
"""
A non-``attrs`` class has been passed into an ``attrs`` function.
.. versionadded:: 16.2.0
"""
class DefaultAlreadySetError(RuntimeError):
"""
A default has been set using ``attr.ib()`` and is attempted to be reset
using the decorator.
.. versionadded:: 17.1.0
"""
class UnannotatedAttributeError(RuntimeError):
"""
A class with ``auto_attribs=True`` has an ``attr.ib()`` without a type
annotation.
.. versionadded:: 17.3.0
"""
class PythonTooOldError(RuntimeError):
"""
It was attempted to use an ``attrs`` feature that requires a newer Python
version.
.. versionadded:: 18.2.0
"""
class NotCallableError(TypeError):
"""
A ``attr.ib()`` requiring a callable has been set with a value
that is not callable.
.. versionadded:: 19.2.0
"""
def __init__(self, msg, value):
super(TypeError, self).__init__(msg, value)
self.msg = msg
self.value = value
def __str__(self):
return str(self.msg)

@ -0,0 +1,17 @@
from typing import Any
class FrozenError(AttributeError):
msg: str = ...
class FrozenInstanceError(FrozenError): ...
class FrozenAttributeError(FrozenError): ...
class AttrsAttributeNotFoundError(ValueError): ...
class NotAnAttrsClassError(ValueError): ...
class DefaultAlreadySetError(RuntimeError): ...
class UnannotatedAttributeError(RuntimeError): ...
class PythonTooOldError(RuntimeError): ...
class NotCallableError(TypeError):
msg: str = ...
value: Any = ...
def __init__(self, msg: str, value: Any) -> None: ...

@ -0,0 +1,51 @@
# SPDX-License-Identifier: MIT
"""
Commonly useful filters for `attr.asdict`.
"""
from ._make import Attribute
def _split_what(what):
"""
Returns a tuple of `frozenset`s of classes and attributes.
"""
return (
frozenset(cls for cls in what if isinstance(cls, type)),
frozenset(cls for cls in what if isinstance(cls, Attribute)),
)
def include(*what):
"""
Include *what*.
:param what: What to include.
:type what: `list` of `type` or `attrs.Attribute`\\ s
:rtype: `callable`
"""
cls, attrs = _split_what(what)
def include_(attribute, value):
return value.__class__ in cls or attribute in attrs
return include_
def exclude(*what):
"""
Exclude *what*.
:param what: What to exclude.
:type what: `list` of classes or `attrs.Attribute`\\ s.
:rtype: `callable`
"""
cls, attrs = _split_what(what)
def exclude_(attribute, value):
return value.__class__ not in cls and attribute not in attrs
return exclude_

@ -0,0 +1,6 @@
from typing import Any, Union
from . import Attribute, _FilterType
def include(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ...
def exclude(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ...

@ -0,0 +1,73 @@
# SPDX-License-Identifier: MIT
"""
Commonly used hooks for on_setattr.
"""
from . import _config
from .exceptions import FrozenAttributeError
def pipe(*setters):
"""
Run all *setters* and return the return value of the last one.
.. versionadded:: 20.1.0
"""
def wrapped_pipe(instance, attrib, new_value):
rv = new_value
for setter in setters:
rv = setter(instance, attrib, rv)
return rv
return wrapped_pipe
def frozen(_, __, ___):
"""
Prevent an attribute to be modified.
.. versionadded:: 20.1.0
"""
raise FrozenAttributeError()
def validate(instance, attrib, new_value):
"""
Run *attrib*'s validator on *new_value* if it has one.
.. versionadded:: 20.1.0
"""
if _config._run_validators is False:
return new_value
v = attrib.validator
if not v:
return new_value
v(instance, attrib, new_value)
return new_value
def convert(instance, attrib, new_value):
"""
Run *attrib*'s converter -- if it has one -- on *new_value* and return the
result.
.. versionadded:: 20.1.0
"""
c = attrib.converter
if c:
return c(new_value)
return new_value
# Sentinel for disabling class-wide *on_setattr* hooks for certain attributes.
# autodata stopped working, so the docstring is inlined in the API docs.
NO_OP = object()

@ -0,0 +1,19 @@
from typing import Any, NewType, NoReturn, TypeVar, cast
from . import Attribute, _OnSetAttrType
_T = TypeVar("_T")
def frozen(
instance: Any, attribute: Attribute[Any], new_value: Any
) -> NoReturn: ...
def pipe(*setters: _OnSetAttrType) -> _OnSetAttrType: ...
def validate(instance: Any, attribute: Attribute[_T], new_value: _T) -> _T: ...
# convert is allowed to return Any, because they can be chained using pipe.
def convert(
instance: Any, attribute: Attribute[Any], new_value: Any
) -> Any: ...
_NoOpType = NewType("_NoOpType", object)
NO_OP: _NoOpType

@ -0,0 +1,594 @@
# SPDX-License-Identifier: MIT
"""
Commonly useful validators.
"""
import operator
import re
from contextlib import contextmanager
from ._config import get_run_validators, set_run_validators
from ._make import _AndValidator, and_, attrib, attrs
from .exceptions import NotCallableError
try:
Pattern = re.Pattern
except AttributeError: # Python <3.7 lacks a Pattern type.
Pattern = type(re.compile(""))
__all__ = [
"and_",
"deep_iterable",
"deep_mapping",
"disabled",
"ge",
"get_disabled",
"gt",
"in_",
"instance_of",
"is_callable",
"le",
"lt",
"matches_re",
"max_len",
"min_len",
"optional",
"provides",
"set_disabled",
]
def set_disabled(disabled):
"""
Globally disable or enable running validators.
By default, they are run.
:param disabled: If ``True``, disable running all validators.
:type disabled: bool
.. warning::
This function is not thread-safe!
.. versionadded:: 21.3.0
"""
set_run_validators(not disabled)
def get_disabled():
"""
Return a bool indicating whether validators are currently disabled or not.
:return: ``True`` if validators are currently disabled.
:rtype: bool
.. versionadded:: 21.3.0
"""
return not get_run_validators()
@contextmanager
def disabled():
"""
Context manager that disables running validators within its context.
.. warning::
This context manager is not thread-safe!
.. versionadded:: 21.3.0
"""
set_run_validators(False)
try:
yield
finally:
set_run_validators(True)
@attrs(repr=False, slots=True, hash=True)
class _InstanceOfValidator:
type = attrib()
def __call__(self, inst, attr, value):
"""
We use a callable class to be able to change the ``__repr__``.
"""
if not isinstance(value, self.type):
raise TypeError(
"'{name}' must be {type!r} (got {value!r} that is a "
"{actual!r}).".format(
name=attr.name,
type=self.type,
actual=value.__class__,
value=value,
),
attr,
self.type,
value,
)
def __repr__(self):
return "<instance_of validator for type {type!r}>".format(
type=self.type
)
def instance_of(type):
"""
A validator that raises a `TypeError` if the initializer is called
with a wrong type for this particular attribute (checks are performed using
`isinstance` therefore it's also valid to pass a tuple of types).
:param type: The type to check for.
:type type: type or tuple of types
:raises TypeError: With a human readable error message, the attribute
(of type `attrs.Attribute`), the expected type, and the value it
got.
"""
return _InstanceOfValidator(type)
@attrs(repr=False, frozen=True, slots=True)
class _MatchesReValidator:
pattern = attrib()
match_func = attrib()
def __call__(self, inst, attr, value):
"""
We use a callable class to be able to change the ``__repr__``.
"""
if not self.match_func(value):
raise ValueError(
"'{name}' must match regex {pattern!r}"
" ({value!r} doesn't)".format(
name=attr.name, pattern=self.pattern.pattern, value=value
),
attr,
self.pattern,
value,
)
def __repr__(self):
return "<matches_re validator for pattern {pattern!r}>".format(
pattern=self.pattern
)
def matches_re(regex, flags=0, func=None):
r"""
A validator that raises `ValueError` if the initializer is called
with a string that doesn't match *regex*.
:param regex: a regex string or precompiled pattern to match against
:param int flags: flags that will be passed to the underlying re function
(default 0)
:param callable func: which underlying `re` function to call. Valid options
are `re.fullmatch`, `re.search`, and `re.match`; the default ``None``
means `re.fullmatch`. For performance reasons, the pattern is always
precompiled using `re.compile`.
.. versionadded:: 19.2.0
.. versionchanged:: 21.3.0 *regex* can be a pre-compiled pattern.
"""
valid_funcs = (re.fullmatch, None, re.search, re.match)
if func not in valid_funcs:
raise ValueError(
"'func' must be one of {}.".format(
", ".join(
sorted(
e and e.__name__ or "None" for e in set(valid_funcs)
)
)
)
)
if isinstance(regex, Pattern):
if flags:
raise TypeError(
"'flags' can only be used with a string pattern; "
"pass flags to re.compile() instead"
)
pattern = regex
else:
pattern = re.compile(regex, flags)
if func is re.match:
match_func = pattern.match
elif func is re.search:
match_func = pattern.search
else:
match_func = pattern.fullmatch
return _MatchesReValidator(pattern, match_func)
@attrs(repr=False, slots=True, hash=True)
class _ProvidesValidator:
interface = attrib()
def __call__(self, inst, attr, value):
"""
We use a callable class to be able to change the ``__repr__``.
"""
if not self.interface.providedBy(value):
raise TypeError(
"'{name}' must provide {interface!r} which {value!r} "
"doesn't.".format(
name=attr.name, interface=self.interface, value=value
),
attr,
self.interface,
value,
)
def __repr__(self):
return "<provides validator for interface {interface!r}>".format(
interface=self.interface
)
def provides(interface):
"""
A validator that raises a `TypeError` if the initializer is called
with an object that does not provide the requested *interface* (checks are
performed using ``interface.providedBy(value)`` (see `zope.interface
<https://zopeinterface.readthedocs.io/en/latest/>`_).
:param interface: The interface to check for.
:type interface: ``zope.interface.Interface``
:raises TypeError: With a human readable error message, the attribute
(of type `attrs.Attribute`), the expected interface, and the
value it got.
"""
return _ProvidesValidator(interface)
@attrs(repr=False, slots=True, hash=True)
class _OptionalValidator:
validator = attrib()
def __call__(self, inst, attr, value):
if value is None:
return
self.validator(inst, attr, value)
def __repr__(self):
return "<optional validator for {what} or None>".format(
what=repr(self.validator)
)
def optional(validator):
"""
A validator that makes an attribute optional. An optional attribute is one
which can be set to ``None`` in addition to satisfying the requirements of
the sub-validator.
:param validator: A validator (or a list of validators) that is used for
non-``None`` values.
:type validator: callable or `list` of callables.
.. versionadded:: 15.1.0
.. versionchanged:: 17.1.0 *validator* can be a list of validators.
"""
if isinstance(validator, list):
return _OptionalValidator(_AndValidator(validator))
return _OptionalValidator(validator)
@attrs(repr=False, slots=True, hash=True)
class _InValidator:
options = attrib()
def __call__(self, inst, attr, value):
try:
in_options = value in self.options
except TypeError: # e.g. `1 in "abc"`
in_options = False
if not in_options:
raise ValueError(
"'{name}' must be in {options!r} (got {value!r})".format(
name=attr.name, options=self.options, value=value
),
attr,
self.options,
value,
)
def __repr__(self):
return "<in_ validator with options {options!r}>".format(
options=self.options
)
def in_(options):
"""
A validator that raises a `ValueError` if the initializer is called
with a value that does not belong in the options provided. The check is
performed using ``value in options``.
:param options: Allowed options.
:type options: list, tuple, `enum.Enum`, ...
:raises ValueError: With a human readable error message, the attribute (of
type `attrs.Attribute`), the expected options, and the value it
got.
.. versionadded:: 17.1.0
.. versionchanged:: 22.1.0
The ValueError was incomplete until now and only contained the human
readable error message. Now it contains all the information that has
been promised since 17.1.0.
"""
return _InValidator(options)
@attrs(repr=False, slots=False, hash=True)
class _IsCallableValidator:
def __call__(self, inst, attr, value):
"""
We use a callable class to be able to change the ``__repr__``.
"""
if not callable(value):
message = (
"'{name}' must be callable "
"(got {value!r} that is a {actual!r})."
)
raise NotCallableError(
msg=message.format(
name=attr.name, value=value, actual=value.__class__
),
value=value,
)
def __repr__(self):
return "<is_callable validator>"
def is_callable():
"""
A validator that raises a `attr.exceptions.NotCallableError` if the
initializer is called with a value for this particular attribute
that is not callable.
.. versionadded:: 19.1.0
:raises `attr.exceptions.NotCallableError`: With a human readable error
message containing the attribute (`attrs.Attribute`) name,
and the value it got.
"""
return _IsCallableValidator()
@attrs(repr=False, slots=True, hash=True)
class _DeepIterable:
member_validator = attrib(validator=is_callable())
iterable_validator = attrib(
default=None, validator=optional(is_callable())
)
def __call__(self, inst, attr, value):
"""
We use a callable class to be able to change the ``__repr__``.
"""
if self.iterable_validator is not None:
self.iterable_validator(inst, attr, value)
for member in value:
self.member_validator(inst, attr, member)
def __repr__(self):
iterable_identifier = (
""
if self.iterable_validator is None
else " {iterable!r}".format(iterable=self.iterable_validator)
)
return (
"<deep_iterable validator for{iterable_identifier}"
" iterables of {member!r}>"
).format(
iterable_identifier=iterable_identifier,
member=self.member_validator,
)
def deep_iterable(member_validator, iterable_validator=None):
"""
A validator that performs deep validation of an iterable.
:param member_validator: Validator(s) to apply to iterable members
:param iterable_validator: Validator to apply to iterable itself
(optional)
.. versionadded:: 19.1.0
:raises TypeError: if any sub-validators fail
"""
if isinstance(member_validator, (list, tuple)):
member_validator = and_(*member_validator)
return _DeepIterable(member_validator, iterable_validator)
@attrs(repr=False, slots=True, hash=True)
class _DeepMapping:
key_validator = attrib(validator=is_callable())
value_validator = attrib(validator=is_callable())
mapping_validator = attrib(default=None, validator=optional(is_callable()))
def __call__(self, inst, attr, value):
"""
We use a callable class to be able to change the ``__repr__``.
"""
if self.mapping_validator is not None:
self.mapping_validator(inst, attr, value)
for key in value:
self.key_validator(inst, attr, key)
self.value_validator(inst, attr, value[key])
def __repr__(self):
return (
"<deep_mapping validator for objects mapping {key!r} to {value!r}>"
).format(key=self.key_validator, value=self.value_validator)
def deep_mapping(key_validator, value_validator, mapping_validator=None):
"""
A validator that performs deep validation of a dictionary.
:param key_validator: Validator to apply to dictionary keys
:param value_validator: Validator to apply to dictionary values
:param mapping_validator: Validator to apply to top-level mapping
attribute (optional)
.. versionadded:: 19.1.0
:raises TypeError: if any sub-validators fail
"""
return _DeepMapping(key_validator, value_validator, mapping_validator)
@attrs(repr=False, frozen=True, slots=True)
class _NumberValidator:
bound = attrib()
compare_op = attrib()
compare_func = attrib()
def __call__(self, inst, attr, value):
"""
We use a callable class to be able to change the ``__repr__``.
"""
if not self.compare_func(value, self.bound):
raise ValueError(
"'{name}' must be {op} {bound}: {value}".format(
name=attr.name,
op=self.compare_op,
bound=self.bound,
value=value,
)
)
def __repr__(self):
return "<Validator for x {op} {bound}>".format(
op=self.compare_op, bound=self.bound
)
def lt(val):
"""
A validator that raises `ValueError` if the initializer is called
with a number larger or equal to *val*.
:param val: Exclusive upper bound for values
.. versionadded:: 21.3.0
"""
return _NumberValidator(val, "<", operator.lt)
def le(val):
"""
A validator that raises `ValueError` if the initializer is called
with a number greater than *val*.
:param val: Inclusive upper bound for values
.. versionadded:: 21.3.0
"""
return _NumberValidator(val, "<=", operator.le)
def ge(val):
"""
A validator that raises `ValueError` if the initializer is called
with a number smaller than *val*.
:param val: Inclusive lower bound for values
.. versionadded:: 21.3.0
"""
return _NumberValidator(val, ">=", operator.ge)
def gt(val):
"""
A validator that raises `ValueError` if the initializer is called
with a number smaller or equal to *val*.
:param val: Exclusive lower bound for values
.. versionadded:: 21.3.0
"""
return _NumberValidator(val, ">", operator.gt)
@attrs(repr=False, frozen=True, slots=True)
class _MaxLengthValidator:
max_length = attrib()
def __call__(self, inst, attr, value):
"""
We use a callable class to be able to change the ``__repr__``.
"""
if len(value) > self.max_length:
raise ValueError(
"Length of '{name}' must be <= {max}: {len}".format(
name=attr.name, max=self.max_length, len=len(value)
)
)
def __repr__(self):
return "<max_len validator for {max}>".format(max=self.max_length)
def max_len(length):
"""
A validator that raises `ValueError` if the initializer is called
with a string or iterable that is longer than *length*.
:param int length: Maximum length of the string or iterable
.. versionadded:: 21.3.0
"""
return _MaxLengthValidator(length)
@attrs(repr=False, frozen=True, slots=True)
class _MinLengthValidator:
min_length = attrib()
def __call__(self, inst, attr, value):
"""
We use a callable class to be able to change the ``__repr__``.
"""
if len(value) < self.min_length:
raise ValueError(
"Length of '{name}' must be => {min}: {len}".format(
name=attr.name, min=self.min_length, len=len(value)
)
)
def __repr__(self):
return "<min_len validator for {min}>".format(min=self.min_length)
def min_len(length):
"""
A validator that raises `ValueError` if the initializer is called
with a string or iterable that is shorter than *length*.
:param int length: Minimum length of the string or iterable
.. versionadded:: 22.1.0
"""
return _MinLengthValidator(length)

@ -0,0 +1,80 @@
from typing import (
Any,
AnyStr,
Callable,
Container,
ContextManager,
Iterable,
List,
Mapping,
Match,
Optional,
Pattern,
Tuple,
Type,
TypeVar,
Union,
overload,
)
from . import _ValidatorType
from . import _ValidatorArgType
_T = TypeVar("_T")
_T1 = TypeVar("_T1")
_T2 = TypeVar("_T2")
_T3 = TypeVar("_T3")
_I = TypeVar("_I", bound=Iterable)
_K = TypeVar("_K")
_V = TypeVar("_V")
_M = TypeVar("_M", bound=Mapping)
def set_disabled(run: bool) -> None: ...
def get_disabled() -> bool: ...
def disabled() -> ContextManager[None]: ...
# To be more precise on instance_of use some overloads.
# If there are more than 3 items in the tuple then we fall back to Any
@overload
def instance_of(type: Type[_T]) -> _ValidatorType[_T]: ...
@overload
def instance_of(type: Tuple[Type[_T]]) -> _ValidatorType[_T]: ...
@overload
def instance_of(
type: Tuple[Type[_T1], Type[_T2]]
) -> _ValidatorType[Union[_T1, _T2]]: ...
@overload
def instance_of(
type: Tuple[Type[_T1], Type[_T2], Type[_T3]]
) -> _ValidatorType[Union[_T1, _T2, _T3]]: ...
@overload
def instance_of(type: Tuple[type, ...]) -> _ValidatorType[Any]: ...
def provides(interface: Any) -> _ValidatorType[Any]: ...
def optional(
validator: Union[_ValidatorType[_T], List[_ValidatorType[_T]]]
) -> _ValidatorType[Optional[_T]]: ...
def in_(options: Container[_T]) -> _ValidatorType[_T]: ...
def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ...
def matches_re(
regex: Union[Pattern[AnyStr], AnyStr],
flags: int = ...,
func: Optional[
Callable[[AnyStr, AnyStr, int], Optional[Match[AnyStr]]]
] = ...,
) -> _ValidatorType[AnyStr]: ...
def deep_iterable(
member_validator: _ValidatorArgType[_T],
iterable_validator: Optional[_ValidatorType[_I]] = ...,
) -> _ValidatorType[_I]: ...
def deep_mapping(
key_validator: _ValidatorType[_K],
value_validator: _ValidatorType[_V],
mapping_validator: Optional[_ValidatorType[_M]] = ...,
) -> _ValidatorType[_M]: ...
def is_callable() -> _ValidatorType[_T]: ...
def lt(val: _T) -> _ValidatorType[_T]: ...
def le(val: _T) -> _ValidatorType[_T]: ...
def ge(val: _T) -> _ValidatorType[_T]: ...
def gt(val: _T) -> _ValidatorType[_T]: ...
def max_len(length: int) -> _ValidatorType[_T]: ...
def min_len(length: int) -> _ValidatorType[_T]: ...

@ -0,0 +1,70 @@
# SPDX-License-Identifier: MIT
from attr import (
NOTHING,
Attribute,
Factory,
__author__,
__copyright__,
__description__,
__doc__,
__email__,
__license__,
__title__,
__url__,
__version__,
__version_info__,
assoc,
cmp_using,
define,
evolve,
field,
fields,
fields_dict,
frozen,
has,
make_class,
mutable,
resolve_types,
validate,
)
from attr._next_gen import asdict, astuple
from . import converters, exceptions, filters, setters, validators
__all__ = [
"__author__",
"__copyright__",
"__description__",
"__doc__",
"__email__",
"__license__",
"__title__",
"__url__",
"__version__",
"__version_info__",
"asdict",
"assoc",
"astuple",
"Attribute",
"cmp_using",
"converters",
"define",
"evolve",
"exceptions",
"Factory",
"field",
"fields_dict",
"fields",
"filters",
"frozen",
"has",
"make_class",
"mutable",
"NOTHING",
"resolve_types",
"setters",
"validate",
"validators",
]

@ -0,0 +1,66 @@
from typing import (
Any,
Callable,
Dict,
Mapping,
Optional,
Sequence,
Tuple,
Type,
)
# Because we need to type our own stuff, we have to make everything from
# attr explicitly public too.
from attr import __author__ as __author__
from attr import __copyright__ as __copyright__
from attr import __description__ as __description__
from attr import __email__ as __email__
from attr import __license__ as __license__
from attr import __title__ as __title__
from attr import __url__ as __url__
from attr import __version__ as __version__
from attr import __version_info__ as __version_info__
from attr import _FilterType
from attr import assoc as assoc
from attr import Attribute as Attribute
from attr import cmp_using as cmp_using
from attr import converters as converters
from attr import define as define
from attr import evolve as evolve
from attr import exceptions as exceptions
from attr import Factory as Factory
from attr import field as field
from attr import fields as fields
from attr import fields_dict as fields_dict
from attr import filters as filters
from attr import frozen as frozen
from attr import has as has
from attr import make_class as make_class
from attr import mutable as mutable
from attr import NOTHING as NOTHING
from attr import resolve_types as resolve_types
from attr import setters as setters
from attr import validate as validate
from attr import validators as validators
# TODO: see definition of attr.asdict/astuple
def asdict(
inst: Any,
recurse: bool = ...,
filter: Optional[_FilterType[Any]] = ...,
dict_factory: Type[Mapping[Any, Any]] = ...,
retain_collection_types: bool = ...,
value_serializer: Optional[
Callable[[type, Attribute[Any], Any], Any]
] = ...,
tuple_keys: bool = ...,
) -> Dict[str, Any]: ...
# TODO: add support for returning NamedTuple from the mypy plugin
def astuple(
inst: Any,
recurse: bool = ...,
filter: Optional[_FilterType[Any]] = ...,
tuple_factory: Type[Sequence[Any]] = ...,
retain_collection_types: bool = ...,
) -> Tuple[Any, ...]: ...

@ -0,0 +1,3 @@
# SPDX-License-Identifier: MIT
from attr.converters import * # noqa

@ -0,0 +1,3 @@
# SPDX-License-Identifier: MIT
from attr.exceptions import * # noqa

@ -0,0 +1,3 @@
# SPDX-License-Identifier: MIT
from attr.filters import * # noqa

@ -0,0 +1,3 @@
# SPDX-License-Identifier: MIT
from attr.setters import * # noqa

@ -0,0 +1,3 @@
# SPDX-License-Identifier: MIT
from attr.validators import * # noqa

@ -1,7 +1,9 @@
# Bazarr dependencies
aniso==9.0.1
argparse==1.4.0
apprise==0.9.8.3
apscheduler==3.8.1
attrs==22.1.0
charamel==1.0.0
deep-translator==1.8.3
dogpile.cache==1.1.5

Loading…
Cancel
Save