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
|
Loading…
Reference in new issue