From 6618712030432d992f957f34e522b792ee74ecc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Deuchnord?= Date: Tue, 7 Apr 2020 21:42:13 +0200 Subject: [PATCH] refactor: simplify ephemerides, remove dead code BREAKING CHANGE: the JSON format has deeply changed to enhance its consistency --- kosmorrolib/data.py | 114 ++++++++++++++------- kosmorrolib/dumper.py | 137 +++++++++++++------------ kosmorrolib/ephemerides.py | 121 +++++++--------------- kosmorrolib/locales/messages.pot | 88 ++++++++-------- kosmorrolib/main.py | 34 ++++--- test/__init__.py | 1 + test/dumper.py | 167 ++++++++++++++++++------------- test/ephemerides.py | 69 +++++++------ test/testutils.py | 48 +++++++++ 9 files changed, 434 insertions(+), 345 deletions(-) create mode 100644 test/testutils.py diff --git a/kosmorrolib/data.py b/kosmorrolib/data.py index 8aee11f..6a66157 100644 --- a/kosmorrolib/data.py +++ b/kosmorrolib/data.py @@ -47,7 +47,13 @@ EVENTS = { } -class MoonPhase: +class Serializable(ABC): + @abstractmethod + def serialize(self) -> dict: + pass + + +class MoonPhase(Serializable): def __init__(self, identifier: str, time: Union[datetime, None], next_phase_date: Union[datetime, None]): if identifier not in MOON_PHASES.keys(): raise ValueError('identifier parameter must be one of %s (got %s)' % (', '.join(MOON_PHASES.keys()), @@ -60,6 +66,11 @@ class MoonPhase: def get_phase(self): return MOON_PHASES[self.identifier] + def get_next_phase_name(self): + next_identifier = self.get_next_phase() + + return MOON_PHASES[next_identifier] + def get_next_phase(self): if self.identifier == 'NEW_MOON' or self.identifier == 'WAXING_CRESCENT': next_identifier = 'FIRST_QUARTER' @@ -69,39 +80,20 @@ class MoonPhase: next_identifier = 'LAST_QUARTER' else: next_identifier = 'NEW_MOON' + return next_identifier - return MOON_PHASES[next_identifier] - - -class Position: - def __init__(self, latitude: float, longitude: float): - self.latitude = latitude - self.longitude = longitude - self.observation_planet = None - self._topos = None - - def get_planet_topos(self) -> Topos: - if self.observation_planet is None: - raise TypeError('Observation planet must be set.') - - if self._topos is None: - self._topos = self.observation_planet + Topos(latitude_degrees=self.latitude, - longitude_degrees=self.longitude) - - return self._topos - - -class AsterEphemerides: - def __init__(self, - rise_time: Union[datetime, None], - culmination_time: Union[datetime, None], - set_time: Union[datetime, None]): - self.rise_time = rise_time - self.culmination_time = culmination_time - self.set_time = set_time + def serialize(self) -> dict: + return { + 'phase': self.identifier, + 'time': self.time.isoformat() if self.time is not None else None, + 'next': { + 'phase': self.get_next_phase(), + 'time': self.next_phase_date.isoformat() + } + } -class Object(ABC): +class Object(Serializable): """ An astronomical object. """ @@ -109,7 +101,6 @@ class Object(ABC): def __init__(self, name: str, skyfield_name: str, - ephemerides: AsterEphemerides or None = None, radius: float = None): """ Initialize an astronomical object @@ -122,7 +113,6 @@ class Object(ABC): self.name = name self.skyfield_name = skyfield_name self.radius = radius - self.ephemerides = ephemerides def get_skyfield_object(self) -> SkfPlanet: return get_skf_objects()[self.skyfield_name] @@ -143,6 +133,13 @@ class Object(ABC): return 360 / pi * arcsin(self.radius / from_place.at(time).observe(self.get_skyfield_object()).distance().km) + def serialize(self) -> dict: + return { + 'name': self.name, + 'type': self.get_type(), + 'radius': self.radius, + } + class Star(Object): def get_type(self) -> str: @@ -164,7 +161,7 @@ class Satellite(Object): return 'satellite' -class Event: +class Event(Serializable): def __init__(self, event_type: str, objects: [Object], start_time: datetime, end_time: Union[datetime, None] = None, details: str = None): if event_type not in EVENTS.keys(): @@ -190,6 +187,15 @@ class Event: return tuple(object.name for object in self.objects) + def serialize(self) -> dict: + return { + 'objects': [object.serialize() for object in self.objects], + 'event': self.event_type, + 'starts_at': self.start_time.isoformat(), + 'ends_at': self.end_time.isoformat() if self.end_time is not None else None, + 'details': self.details + } + def skyfield_to_moon_phase(times: [Time], vals: [int], now: Time) -> Union[MoonPhase, None]: tomorrow = get_timescale().utc(now.utc_datetime().year, now.utc_datetime().month, now.utc_datetime().day + 1) @@ -228,8 +234,30 @@ def skyfield_to_moon_phase(times: [Time], vals: [int], now: Time) -> Union[MoonP next_phase_time.utc_datetime() if next_phase_time is not None else None) +class AsterEphemerides(Serializable): + def __init__(self, + rise_time: Union[datetime, None], + culmination_time: Union[datetime, None], + set_time: Union[datetime, None], + aster: Object): + self.rise_time = rise_time + self.culmination_time = culmination_time + self.set_time = set_time + self.object = aster + + def serialize(self) -> dict: + return { + 'object': self.object.serialize(), + 'rise_time': self.rise_time.isoformat() if self.rise_time is not None else None, + 'culmination_time': self.culmination_time.isoformat() if self.culmination_time is not None else None, + 'set_time': self.set_time.isoformat() if self.set_time is not None else None + } + + MONTHS = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'] +EARTH = Planet('Earth', 'EARTH') + ASTERS = [Star(_('Sun'), 'SUN', radius=696342), Satellite(_('Moon'), 'MOON', radius=1737.4), Planet(_('Mercury'), 'MERCURY', radius=2439.7), @@ -240,3 +268,21 @@ ASTERS = [Star(_('Sun'), 'SUN', radius=696342), Planet(_('Uranus'), 'URANUS BARYCENTER', radius=25559), Planet(_('Neptune'), 'NEPTUNE BARYCENTER', radius=24764), Planet(_('Pluto'), 'PLUTO BARYCENTER', radius=1185)] + + +class Position: + def __init__(self, latitude: float, longitude: float, aster: Object): + self.latitude = latitude + self.longitude = longitude + self.aster = aster + self._topos = None + + def get_planet_topos(self) -> Topos: + if self.aster is None: + raise TypeError('Observation planet must be set.') + + if self._topos is None: + self._topos = self.aster.get_skyfield_object() + Topos(latitude_degrees=self.latitude, + longitude_degrees=self.longitude) + + return self._topos diff --git a/kosmorrolib/dumper.py b/kosmorrolib/dumper.py index 161d4e3..1c75ecd 100644 --- a/kosmorrolib/dumper.py +++ b/kosmorrolib/dumper.py @@ -40,9 +40,10 @@ TIME_FORMAT = _('{hours}:{minutes}').format(hours='%H', minutes='%M') class Dumper(ABC): - def __init__(self, ephemeris: dict, events: [Event], date: datetime.date = datetime.date.today(), timezone: int = 0, - with_colors: bool = True): - self.ephemeris = ephemeris + def __init__(self, ephemerides: [AsterEphemerides] = None, moon_phase: MoonPhase = None, events: [Event] = None, + date: datetime.date = datetime.date.today(), timezone: int = 0, with_colors: bool = True): + self.ephemerides = ephemerides + self.moon_phase = moon_phase self.events = events self.date = date self.timezone = timezone @@ -52,19 +53,20 @@ class Dumper(ABC): self._convert_dates_to_timezones() def _convert_dates_to_timezones(self): - if self.ephemeris['moon_phase'].time is not None: - self.ephemeris['moon_phase'].time = self._datetime_to_timezone(self.ephemeris['moon_phase'].time) - if self.ephemeris['moon_phase'].next_phase_date is not None: - self.ephemeris['moon_phase'].next_phase_date = self._datetime_to_timezone( - self.ephemeris['moon_phase'].next_phase_date) - - for aster in self.ephemeris['details']: - if aster.ephemerides.rise_time is not None: - aster.ephemerides.rise_time = self._datetime_to_timezone(aster.ephemerides.rise_time) - if aster.ephemerides.culmination_time is not None: - aster.ephemerides.culmination_time = self._datetime_to_timezone(aster.ephemerides.culmination_time) - if aster.ephemerides.set_time is not None: - aster.ephemerides.set_time = self._datetime_to_timezone(aster.ephemerides.set_time) + if self.moon_phase.time is not None: + self.moon_phase.time = self._datetime_to_timezone(self.moon_phase.time) + if self.moon_phase.next_phase_date is not None: + self.moon_phase.next_phase_date = self._datetime_to_timezone( + self.moon_phase.next_phase_date) + + if self.ephemerides is not None: + for ephemeris in self.ephemerides: + if ephemeris.rise_time is not None: + ephemeris.rise_time = self._datetime_to_timezone(ephemeris.rise_time) + if ephemeris.culmination_time is not None: + ephemeris.culmination_time = self._datetime_to_timezone(ephemeris.culmination_time) + if ephemeris.set_time is not None: + ephemeris.set_time = self._datetime_to_timezone(ephemeris.set_time) for event in self.events: event.start_time = self._datetime_to_timezone(event.start_time) @@ -99,11 +101,11 @@ class Dumper(ABC): class JsonDumper(Dumper): def to_string(self): - self.ephemeris['events'] = self.events - self.ephemeris['ephemerides'] = self.ephemeris.pop('details') - return json.dumps(self.ephemeris, - default=self._json_default, - indent=4) + return json.dumps({ + 'ephemerides': [ephemeris.serialize() for ephemeris in self.ephemerides], + 'moon_phase': self.moon_phase.serialize(), + 'events': [event.serialize() for event in self.events] + }, indent=4) @staticmethod def _json_default(obj): @@ -139,10 +141,10 @@ class TextDumper(Dumper): def to_string(self): text = [self.style(self.get_date_as_string(capitalized=True), 'h1')] - if len(self.ephemeris['details']) > 0: - text.append(self.get_asters(self.ephemeris['details'])) + if self.ephemerides is not None: + text.append(self.stringify_ephemerides()) - text.append(self.get_moon(self.ephemeris['moon_phase'])) + text.append(self.get_moon(self.moon_phase)) if len(self.events) > 0: text.append('\n'.join([self.style(_('Expected events:'), 'h2'), @@ -173,28 +175,28 @@ class TextDumper(Dumper): return styles[tag](text) - def get_asters(self, asters: [Object]) -> str: + def stringify_ephemerides(self) -> str: data = [] - for aster in asters: - name = self.style(aster.name, 'th') + for ephemeris in self.ephemerides: + name = self.style(ephemeris.object.name, 'th') - if aster.ephemerides.rise_time is not None: - time_fmt = TIME_FORMAT if aster.ephemerides.rise_time.day == self.date.day else SHORT_DATETIME_FORMAT - planet_rise = aster.ephemerides.rise_time.strftime(time_fmt) + if ephemeris.rise_time is not None: + time_fmt = TIME_FORMAT if ephemeris.rise_time.day == self.date.day else SHORT_DATETIME_FORMAT + planet_rise = ephemeris.rise_time.strftime(time_fmt) else: planet_rise = '-' - if aster.ephemerides.culmination_time is not None: - time_fmt = TIME_FORMAT if aster.ephemerides.culmination_time.day == self.date.day \ + if ephemeris.culmination_time is not None: + time_fmt = TIME_FORMAT if ephemeris.culmination_time.day == self.date.day \ else SHORT_DATETIME_FORMAT - planet_culmination = aster.ephemerides.culmination_time.strftime(time_fmt) + planet_culmination = ephemeris.culmination_time.strftime(time_fmt) else: planet_culmination = '-' - if aster.ephemerides.set_time is not None: - time_fmt = TIME_FORMAT if aster.ephemerides.set_time.day == self.date.day else SHORT_DATETIME_FORMAT - planet_set = aster.ephemerides.set_time.strftime(time_fmt) + if ephemeris.set_time is not None: + time_fmt = TIME_FORMAT if ephemeris.set_time.day == self.date.day else SHORT_DATETIME_FORMAT + planet_set = ephemeris.set_time.strftime(time_fmt) else: planet_set = '-' @@ -219,7 +221,7 @@ class TextDumper(Dumper): def get_moon(self, moon_phase: MoonPhase) -> str: current_moon_phase = ' '.join([self.style(_('Moon phase:'), 'strong'), moon_phase.get_phase()]) new_moon_phase = _('{next_moon_phase} on {next_moon_phase_date} at {next_moon_phase_time}').format( - next_moon_phase=moon_phase.get_next_phase(), + next_moon_phase=moon_phase.get_next_phase_name(), next_moon_phase_date=moon_phase.next_phase_date.strftime(FULL_DATE_FORMAT), next_moon_phase_time=moon_phase.next_phase_date.strftime(TIME_FORMAT) ) @@ -242,12 +244,12 @@ class _LatexDumper(Dumper): 'assets', 'png', 'kosmorro-logo.png') moon_phase_graphics = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'assets', 'moonphases', 'png', - '.'.join([self.ephemeris['moon_phase'].identifier.lower().replace('_', '-'), + '.'.join([self.moon_phase.identifier.lower().replace('_', '-'), 'png'])) document = template - if len(self.ephemeris['details']) == 0: + if self.ephemerides is None: document = self._remove_section(document, 'ephemerides') if len(self.events) == 0: @@ -276,7 +278,7 @@ class _LatexDumper(Dumper): .replace('+++EPHEMERIDES+++', self._make_ephemerides()) \ .replace('+++MOON-PHASE-GRAPHICS+++', moon_phase_graphics) \ .replace('+++CURRENT-MOON-PHASE-TITLE+++', _('Moon phase:')) \ - .replace('+++CURRENT-MOON-PHASE+++', self.ephemeris['moon_phase'].get_phase()) \ + .replace('+++CURRENT-MOON-PHASE+++', self.moon_phase.get_phase()) \ .replace('+++SECTION-EVENTS+++', _('Expected events')) \ .replace('+++EVENTS+++', self._make_events()) @@ -285,30 +287,31 @@ class _LatexDumper(Dumper): def _make_ephemerides(self) -> str: latex = [] - for aster in self.ephemeris['details']: - if aster.ephemerides.rise_time is not None: - time_fmt = TIME_FORMAT if aster.ephemerides.rise_time.day == self.date.day else SHORT_DATETIME_FORMAT - aster_rise = aster.ephemerides.rise_time.strftime(time_fmt) - else: - aster_rise = '-' - - if aster.ephemerides.culmination_time is not None: - time_fmt = TIME_FORMAT if aster.ephemerides.culmination_time.day == self.date.day\ - else SHORT_DATETIME_FORMAT - aster_culmination = aster.ephemerides.culmination_time.strftime(time_fmt) - else: - aster_culmination = '-' - - if aster.ephemerides.set_time is not None: - time_fmt = TIME_FORMAT if aster.ephemerides.set_time.day == self.date.day else SHORT_DATETIME_FORMAT - aster_set = aster.ephemerides.set_time.strftime(time_fmt) - else: - aster_set = '-' - - latex.append(r'\object{%s}{%s}{%s}{%s}' % (aster.name, - aster_rise, - aster_culmination, - aster_set)) + if self.ephemerides is not None: + for ephemeris in self.ephemerides: + if ephemeris.rise_time is not None: + time_fmt = TIME_FORMAT if ephemeris.rise_time.day == self.date.day else SHORT_DATETIME_FORMAT + aster_rise = ephemeris.rise_time.strftime(time_fmt) + else: + aster_rise = '-' + + if ephemeris.culmination_time is not None: + time_fmt = TIME_FORMAT if ephemeris.culmination_time.day == self.date.day\ + else SHORT_DATETIME_FORMAT + aster_culmination = ephemeris.culmination_time.strftime(time_fmt) + else: + aster_culmination = '-' + + if ephemeris.set_time is not None: + time_fmt = TIME_FORMAT if ephemeris.set_time.day == self.date.day else SHORT_DATETIME_FORMAT + aster_set = ephemeris.set_time.strftime(time_fmt) + else: + aster_set = '-' + + latex.append(r'\object{%s}{%s}{%s}{%s}' % (ephemeris.object.name, + aster_rise, + aster_culmination, + aster_set)) return ''.join(latex) @@ -342,13 +345,9 @@ class _LatexDumper(Dumper): class PdfDumper(Dumper): - def __init__(self, ephemerides, events, date=datetime.datetime.now(), timezone=0, with_colors=True): - super(PdfDumper, self).__init__(ephemerides, events, date=date, timezone=0, with_colors=with_colors) - self.timezone = timezone - def to_string(self): try: - latex_dumper = _LatexDumper(self.ephemeris, self.events, + latex_dumper = _LatexDumper(self.ephemerides, self.moon_phase, self.events, date=self.date, timezone=self.timezone, with_colors=self.with_colors) return self._compile(latex_dumper.to_string()) except RuntimeError: diff --git a/kosmorrolib/ephemerides.py b/kosmorrolib/ephemerides.py index 1c98309..b03dd3e 100644 --- a/kosmorrolib/ephemerides.py +++ b/kosmorrolib/ephemerides.py @@ -17,77 +17,63 @@ # along with this program. If not, see . import datetime -from typing import Union -from skyfield import almanac from skyfield.searchlib import find_discrete, find_maxima from skyfield.timelib import Time from skyfield.constants import tau -from .data import Object, Position, AsterEphemerides, MoonPhase, ASTERS, skyfield_to_moon_phase +from .data import Position, AsterEphemerides, MoonPhase, Object, ASTERS, skyfield_to_moon_phase from .core import get_skf_objects, get_timescale, get_iau2000b RISEN_ANGLE = -0.8333 -class EphemeridesComputer: - def __init__(self, position: Union[Position, None]): - if position is not None: - position.observation_planet = get_skf_objects()['earth'] - self.position = position +def get_moon_phase(compute_date: datetime.date) -> MoonPhase: + earth = get_skf_objects()['earth'] + moon = get_skf_objects()['moon'] + sun = get_skf_objects()['sun'] - def get_sun(self, start_time, end_time) -> dict: - times, is_risen = find_discrete(start_time, - end_time, - almanac.sunrise_sunset(get_skf_objects(), self.position)) + def moon_phase_at(time: Time): + time._nutation_angles = get_iau2000b(time) + current_earth = earth.at(time) + _, mlon, _ = current_earth.observe(moon).apparent().ecliptic_latlon('date') + _, slon, _ = current_earth.observe(sun).apparent().ecliptic_latlon('date') + return (((mlon.radians - slon.radians) // (tau / 8)) % 8).astype(int) - sunrise = times[0] if is_risen[0] else times[1] - sunset = times[1] if not is_risen[1] else times[0] + moon_phase_at.rough_period = 7.0 # one lunar phase per week - return {'rise': sunrise, 'set': sunset} + today = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day) + time1 = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day - 10) + time2 = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day + 10) - @staticmethod - def get_moon_phase(compute_date: datetime.date) -> MoonPhase: - earth = get_skf_objects()['earth'] - moon = get_skf_objects()['moon'] - sun = get_skf_objects()['sun'] + times, phase = find_discrete(time1, time2, moon_phase_at) - def moon_phase_at(time: Time): - time._nutation_angles = get_iau2000b(time) - current_earth = earth.at(time) - _, mlon, _ = current_earth.observe(moon).apparent().ecliptic_latlon('date') - _, slon, _ = current_earth.observe(sun).apparent().ecliptic_latlon('date') - return (((mlon.radians - slon.radians) // (tau / 8)) % 8).astype(int) + return skyfield_to_moon_phase(times, phase, today) - moon_phase_at.rough_period = 7.0 # one lunar phase per week - today = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day) - time1 = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day - 10) - time2 = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day + 10) +def get_ephemerides(date: datetime.date, position: Position) -> [AsterEphemerides]: + ephemerides = [] - times, phase = find_discrete(time1, time2, moon_phase_at) + def get_angle(for_aster: Object): + def fun(time: Time) -> float: + return position.get_planet_topos().at(time).observe(for_aster.get_skyfield_object()).apparent().altaz()[0]\ + .degrees + fun.rough_period = 1.0 + return fun - return skyfield_to_moon_phase(times, phase, today) + def is_risen(for_aster: Object): + def fun(time: Time) -> bool: + return get_angle(for_aster)(time) > RISEN_ANGLE + fun.rough_period = 0.5 + return fun - @staticmethod - def get_asters_ephemerides_for_aster(aster, date: datetime.date, position: Position) -> Object: - skyfield_aster = get_skf_objects()[aster.skyfield_name] + start_time = get_timescale().utc(date.year, date.month, date.day) + end_time = get_timescale().utc(date.year, date.month, date.day, 23, 59, 59) - def get_angle(time: Time) -> float: - return position.get_planet_topos().at(time).observe(skyfield_aster).apparent().altaz()[0].degrees - - def is_risen(time: Time) -> bool: - return get_angle(time) > RISEN_ANGLE - - get_angle.rough_period = 1.0 - is_risen.rough_period = 0.5 - - start_time = get_timescale().utc(date.year, date.month, date.day) - end_time = get_timescale().utc(date.year, date.month, date.day, 23, 59, 59) - - rise_times, arr = find_discrete(start_time, end_time, is_risen) + for aster in ASTERS: + rise_times, arr = find_discrete(start_time, end_time, is_risen(aster)) try: - culmination_time, _ = find_maxima(start_time, end_time, f=get_angle, epsilon=1./3600/24, num=12) + culmination_time, _ = find_maxima(start_time, end_time, f=get_angle(aster), epsilon=1./3600/24, num=12) culmination_time = culmination_time[0] if len(culmination_time) > 0 else None except ValueError: culmination_time = None @@ -109,37 +95,6 @@ class EphemeridesComputer: if set_time is not None: set_time = set_time.utc_datetime().replace(microsecond=0) if set_time is not None else None - aster.ephemerides = AsterEphemerides(rise_time, culmination_time, set_time) - return aster - - @staticmethod - def is_leap_year(year: int) -> bool: - return (year % 4 == 0 and year % 100 > 0) or (year % 400 == 0) - - def compute_ephemerides(self, compute_date: datetime.date) -> dict: - return {'moon_phase': self.get_moon_phase(compute_date), - 'details': [self.get_asters_ephemerides_for_aster(aster, compute_date, self.position) - for aster in ASTERS] if self.position is not None else []} - - @staticmethod - def get_seasons(year: int) -> dict: - start_time = get_timescale().utc(year, 1, 1) - end_time = get_timescale().utc(year, 12, 31) - times, almanac_seasons = find_discrete(start_time, end_time, almanac.seasons(get_skf_objects())) - - seasons = {} - for time, almanac_season in zip(times, almanac_seasons): - if almanac_season == 0: - season = 'MARCH' - elif almanac_season == 1: - season = 'JUNE' - elif almanac_season == 2: - season = 'SEPTEMBER' - elif almanac_season == 3: - season = 'DECEMBER' - else: - raise AssertionError - - seasons[season] = time.utc_iso() - - return seasons + ephemerides.append(AsterEphemerides(rise_time, culmination_time, set_time, aster=aster)) + + return ephemerides diff --git a/kosmorrolib/locales/messages.pot b/kosmorrolib/locales/messages.pot index c895575..841a531 100644 --- a/kosmorrolib/locales/messages.pot +++ b/kosmorrolib/locales/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: kosmorro 0.7.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2020-04-16 17:57+0200\n" +"POT-Creation-Date: 2020-04-18 15:51+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -79,43 +79,43 @@ msgstr "" msgid "%s's largest elongation" msgstr "" -#: kosmorrolib/data.py:233 +#: kosmorrolib/data.py:261 msgid "Sun" msgstr "" -#: kosmorrolib/data.py:234 +#: kosmorrolib/data.py:262 msgid "Moon" msgstr "" -#: kosmorrolib/data.py:235 +#: kosmorrolib/data.py:263 msgid "Mercury" msgstr "" -#: kosmorrolib/data.py:236 +#: kosmorrolib/data.py:264 msgid "Venus" msgstr "" -#: kosmorrolib/data.py:237 +#: kosmorrolib/data.py:265 msgid "Mars" msgstr "" -#: kosmorrolib/data.py:238 +#: kosmorrolib/data.py:266 msgid "Jupiter" msgstr "" -#: kosmorrolib/data.py:239 +#: kosmorrolib/data.py:267 msgid "Saturn" msgstr "" -#: kosmorrolib/data.py:240 +#: kosmorrolib/data.py:268 msgid "Uranus" msgstr "" -#: kosmorrolib/data.py:241 +#: kosmorrolib/data.py:269 msgid "Neptune" msgstr "" -#: kosmorrolib/data.py:242 +#: kosmorrolib/data.py:270 msgid "Pluto" msgstr "" @@ -131,68 +131,68 @@ msgstr "" msgid "{hours}:{minutes}" msgstr "" -#: kosmorrolib/dumper.py:148 +#: kosmorrolib/dumper.py:150 msgid "Expected events:" msgstr "" -#: kosmorrolib/dumper.py:152 +#: kosmorrolib/dumper.py:154 msgid "Note: All the hours are given in UTC." msgstr "" -#: kosmorrolib/dumper.py:157 +#: kosmorrolib/dumper.py:159 msgid "Note: All the hours are given in the UTC{offset} timezone." msgstr "" -#: kosmorrolib/dumper.py:203 kosmorrolib/dumper.py:272 +#: kosmorrolib/dumper.py:205 kosmorrolib/dumper.py:274 msgid "Object" msgstr "" -#: kosmorrolib/dumper.py:204 kosmorrolib/dumper.py:273 +#: kosmorrolib/dumper.py:206 kosmorrolib/dumper.py:275 msgid "Rise time" msgstr "" -#: kosmorrolib/dumper.py:205 kosmorrolib/dumper.py:274 +#: kosmorrolib/dumper.py:207 kosmorrolib/dumper.py:276 msgid "Culmination time" msgstr "" -#: kosmorrolib/dumper.py:206 kosmorrolib/dumper.py:275 +#: kosmorrolib/dumper.py:208 kosmorrolib/dumper.py:277 msgid "Set time" msgstr "" -#: kosmorrolib/dumper.py:220 kosmorrolib/dumper.py:278 +#: kosmorrolib/dumper.py:222 kosmorrolib/dumper.py:280 msgid "Moon phase:" msgstr "" -#: kosmorrolib/dumper.py:221 +#: kosmorrolib/dumper.py:223 msgid "{next_moon_phase} on {next_moon_phase_date} at {next_moon_phase_time}" msgstr "" -#: kosmorrolib/dumper.py:259 +#: kosmorrolib/dumper.py:261 msgid "A Summary of your Sky" msgstr "" -#: kosmorrolib/dumper.py:263 +#: kosmorrolib/dumper.py:265 msgid "" "This document summarizes the ephemerides and the events of {date}. It " "aims to help you to prepare your observation session. All the hours are " "given in {timezone}." msgstr "" -#: kosmorrolib/dumper.py:269 +#: kosmorrolib/dumper.py:271 msgid "" "Don't forget to check the weather forecast before you go out with your " "equipment." msgstr "" -#: kosmorrolib/dumper.py:271 +#: kosmorrolib/dumper.py:273 msgid "Ephemerides of the day" msgstr "" -#: kosmorrolib/dumper.py:280 +#: kosmorrolib/dumper.py:282 msgid "Expected events" msgstr "" -#: kosmorrolib/dumper.py:355 +#: kosmorrolib/dumper.py:354 msgid "" "Building PDFs was not possible, because some dependencies are not " "installed.\n" @@ -200,93 +200,93 @@ msgid "" "information." msgstr "" -#: kosmorrolib/main.py:58 +#: kosmorrolib/main.py:61 msgid "" "Save the planet and paper!\n" "Consider printing you PDF document only if really necessary, and use the " "other side of the sheet." msgstr "" -#: kosmorrolib/main.py:62 +#: kosmorrolib/main.py:65 msgid "" "PDF output will not contain the ephemerides, because you didn't provide " "the observation coordinate." msgstr "" -#: kosmorrolib/main.py:91 +#: kosmorrolib/main.py:93 msgid "Could not save the output in \"{path}\": {error}" msgstr "" -#: kosmorrolib/main.py:96 +#: kosmorrolib/main.py:98 msgid "Selected output format needs an output file (--output)." msgstr "" -#: kosmorrolib/main.py:115 +#: kosmorrolib/main.py:117 msgid "Running on Python {python_version}" msgstr "" -#: kosmorrolib/main.py:121 +#: kosmorrolib/main.py:123 msgid "Do you really want to clear Kosmorro's cache? [yN] " msgstr "" -#: kosmorrolib/main.py:128 +#: kosmorrolib/main.py:130 msgid "Answer did not match expected options, cache not cleared." msgstr "" -#: kosmorrolib/main.py:137 +#: kosmorrolib/main.py:139 msgid "" "Compute the ephemerides and the events for a given date, at a given " "position on Earth." msgstr "" -#: kosmorrolib/main.py:139 +#: kosmorrolib/main.py:141 msgid "" "By default, only the events will be computed for today ({date}).\n" "To compute also the ephemerides, latitude and longitude arguments are " "needed." msgstr "" -#: kosmorrolib/main.py:144 +#: kosmorrolib/main.py:146 msgid "Show the program version" msgstr "" -#: kosmorrolib/main.py:146 +#: kosmorrolib/main.py:148 msgid "Delete all the files Kosmorro stored in the cache." msgstr "" -#: kosmorrolib/main.py:148 +#: kosmorrolib/main.py:150 msgid "The format under which the information have to be output" msgstr "" -#: kosmorrolib/main.py:150 +#: kosmorrolib/main.py:152 msgid "" "The observer's latitude on Earth. Can also be set in the " "KOSMORRO_LATITUDE environment variable." msgstr "" -#: kosmorrolib/main.py:153 +#: kosmorrolib/main.py:155 msgid "" "The observer's longitude on Earth. Can also be set in the " "KOSMORRO_LONGITUDE environment variable." msgstr "" -#: kosmorrolib/main.py:156 +#: kosmorrolib/main.py:158 msgid "" "The date for which the ephemerides must be computed (in the YYYY-MM-DD " "format). Defaults to the current date ({default_date})" msgstr "" -#: kosmorrolib/main.py:160 +#: kosmorrolib/main.py:162 msgid "" "The timezone to display the hours in (e.g. 2 for UTC+2 or -3 for UTC-3). " "Can also be set in the KOSMORRO_TIMEZONE environment variable." msgstr "" -#: kosmorrolib/main.py:163 +#: kosmorrolib/main.py:165 msgid "Disable the colors in the console." msgstr "" -#: kosmorrolib/main.py:165 +#: kosmorrolib/main.py:167 msgid "" "A file to export the output to. If not given, the standard output is " "used. This argument is needed for PDF format." diff --git a/kosmorrolib/main.py b/kosmorrolib/main.py index 97a1499..3f23b65 100644 --- a/kosmorrolib/main.py +++ b/kosmorrolib/main.py @@ -24,19 +24,22 @@ import sys from datetime import date from termcolor import colored -from kosmorrolib.version import VERSION -from kosmorrolib import dumper -from kosmorrolib import core -from kosmorrolib import events -from kosmorrolib.i18n import _ -from .ephemerides import EphemeridesComputer, Position +from . import dumper +from . import core +from . import events + +from .data import Position, EARTH from .exceptions import UnavailableFeatureError +from .i18n import _ +from . import ephemerides +from .version import VERSION def main(): environment = core.get_env() output_formats = get_dumpers() args = get_args(list(output_formats.keys())) + output_format = args.format if args.special_action is not None: return 0 if args.special_action() else 1 @@ -50,11 +53,11 @@ def main(): position = None if args.latitude is not None or args.longitude is not None: - position = Position(args.latitude, args.longitude) + position = Position(args.latitude, args.longitude, EARTH) elif environment.latitude is not None and environment.longitude is not None: - position = Position(float(environment.latitude), float(environment.longitude)) + position = Position(float(environment.latitude), float(environment.longitude), EARTH) - if args.format == 'pdf': + if output_format == 'pdf': print(_('Save the planet and paper!\n' 'Consider printing you PDF document only if really necessary, and use the other side of the sheet.')) if position is None: @@ -63,8 +66,8 @@ def main(): "coordinate."), 'yellow')) try: - ephemeris = EphemeridesComputer(position) - ephemerides = ephemeris.compute_ephemerides(compute_date) + eph = ephemerides.get_ephemerides(date=compute_date, position=position) if position is not None else None + moon_phase = ephemerides.get_moon_phase(compute_date) events_list = events.search_events(compute_date) @@ -75,10 +78,9 @@ def main(): elif timezone is None: timezone = 0 - selected_dumper = output_formats[args.format](ephemerides, events_list, - date=compute_date, timezone=timezone, - with_colors=args.colors) - output = selected_dumper.to_string() + format_dumper = output_formats[output_format](ephemerides=eph, moon_phase=moon_phase, events=events_list, + date=compute_date, timezone=timezone, with_colors=args.colors) + output = format_dumper.to_string() except UnavailableFeatureError as error: print(colored(error.msg, 'red')) return 2 @@ -90,7 +92,7 @@ def main(): except OSError as error: print(_('Could not save the output in "{path}": {error}').format(path=args.output, error=error.strerror)) - elif not selected_dumper.is_file_output_needed(): + elif not format_dumper.is_file_output_needed(): print(output) else: print(colored(_('Selected output format needs an output file (--output).'), color='red')) diff --git a/test/__init__.py b/test/__init__.py index cfb0205..16c4773 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -3,3 +3,4 @@ from .data import * from .dumper import * from .ephemerides import * from .events import * +from .testutils import * diff --git a/test/dumper.py b/test/dumper.py index de02454..63a0f95 100644 --- a/test/dumper.py +++ b/test/dumper.py @@ -11,86 +11,110 @@ class DumperTestCase(unittest.TestCase): def test_json_dumper_returns_correct_json(self): self.assertEqual('{\n' + ' "ephemerides": [\n' + ' {\n' + ' "object": {\n' + ' "name": "Mars",\n' + ' "type": "planet",\n' + ' "radius": null\n' + ' },\n' + ' "rise_time": null,\n' + ' "culmination_time": null,\n' + ' "set_time": null\n' + ' }\n' + ' ],\n' ' "moon_phase": {\n' - ' "next_phase_date": "2019-10-21T00:00:00",\n' ' "phase": "FULL_MOON",\n' - ' "date": "2019-10-14T00:00:00"\n' + ' "time": "2019-10-14T00:00:00",\n' + ' "next": {\n' + ' "phase": "LAST_QUARTER",\n' + ' "time": "2019-10-21T00:00:00"\n' + ' }\n' ' },\n' ' "events": [\n' ' {\n' - ' "event_type": "OPPOSITION",\n' ' "objects": [\n' - ' "Mars"\n' + ' {\n' + ' "name": "Mars",\n' + ' "type": "planet",\n' + ' "radius": null\n' + ' }\n' ' ],\n' - ' "start_time": "2019-10-14T23:00:00",\n' - ' "end_time": null,\n' + ' "event": "OPPOSITION",\n' + ' "starts_at": "2019-10-14T23:00:00",\n' + ' "ends_at": null,\n' ' "details": null\n' ' },\n' ' {\n' - ' "event_type": "MAXIMAL_ELONGATION",\n' ' "objects": [\n' - ' "Venus"\n' + ' {\n' + ' "name": "Venus",\n' + ' "type": "planet",\n' + ' "radius": null\n' + ' }\n' ' ],\n' - ' "start_time": "2019-10-14T12:00:00",\n' - ' "end_time": null,\n' + ' "event": "MAXIMAL_ELONGATION",\n' + ' "starts_at": "2019-10-14T12:00:00",\n' + ' "ends_at": null,\n' ' "details": "42.0\\u00b0"\n' ' }\n' - ' ],\n' - ' "ephemerides": [\n' - ' {\n' - ' "object": "Mars",\n' - ' "details": {\n' - ' "rise_time": null,\n' - ' "culmination_time": null,\n' - ' "set_time": null\n' - ' }\n' - ' }\n' ' ]\n' - '}', JsonDumper(self._get_data(), self._get_events()).to_string()) + '}', JsonDumper(self._get_ephemerides(), self._get_moon_phase(), self._get_events()).to_string()) - data = self._get_data(aster_rise_set=True) self.assertEqual('{\n' + ' "ephemerides": [\n' + ' {\n' + ' "object": {\n' + ' "name": "Mars",\n' + ' "type": "planet",\n' + ' "radius": null\n' + ' },\n' + ' "rise_time": "2019-10-14T08:00:00",\n' + ' "culmination_time": "2019-10-14T13:00:00",\n' + ' "set_time": "2019-10-14T23:00:00"\n' + ' }\n' + ' ],\n' ' "moon_phase": {\n' - ' "next_phase_date": "2019-10-21T00:00:00",\n' ' "phase": "FULL_MOON",\n' - ' "date": "2019-10-14T00:00:00"\n' + ' "time": "2019-10-14T00:00:00",\n' + ' "next": {\n' + ' "phase": "LAST_QUARTER",\n' + ' "time": "2019-10-21T00:00:00"\n' + ' }\n' ' },\n' ' "events": [\n' ' {\n' - ' "event_type": "OPPOSITION",\n' ' "objects": [\n' - ' "Mars"\n' + ' {\n' + ' "name": "Mars",\n' + ' "type": "planet",\n' + ' "radius": null\n' + ' }\n' ' ],\n' - ' "start_time": "2019-10-14T23:00:00",\n' - ' "end_time": null,\n' + ' "event": "OPPOSITION",\n' + ' "starts_at": "2019-10-14T23:00:00",\n' + ' "ends_at": null,\n' ' "details": null\n' ' },\n' ' {\n' - ' "event_type": "MAXIMAL_ELONGATION",\n' ' "objects": [\n' - ' "Venus"\n' + ' {\n' + ' "name": "Venus",\n' + ' "type": "planet",\n' + ' "radius": null\n' + ' }\n' ' ],\n' - ' "start_time": "2019-10-14T12:00:00",\n' - ' "end_time": null,\n' + ' "event": "MAXIMAL_ELONGATION",\n' + ' "starts_at": "2019-10-14T12:00:00",\n' + ' "ends_at": null,\n' ' "details": "42.0\\u00b0"\n' ' }\n' - ' ],\n' - ' "ephemerides": [\n' - ' {\n' - ' "object": "Mars",\n' - ' "details": {\n' - ' "rise_time": "2019-10-14T08:00:00",\n' - ' "culmination_time": "2019-10-14T13:00:00",\n' - ' "set_time": "2019-10-14T23:00:00"\n' - ' }\n' - ' }\n' ' ]\n' - '}', JsonDumper(data, - self._get_events() - ).to_string()) + '}', JsonDumper(self._get_ephemerides(aster_rise_set=True), self._get_moon_phase(), + self._get_events()).to_string()) def test_text_dumper_without_events(self): - ephemerides = self._get_data() + ephemerides = self._get_ephemerides() self.assertEqual('Monday October 14, 2019\n\n' 'Object Rise time Culmination time Set time\n' '-------- ----------- ------------------ ----------\n' @@ -98,9 +122,9 @@ class DumperTestCase(unittest.TestCase): 'Moon phase: Full Moon\n' 'Last Quarter on Monday October 21, 2019 at 00:00\n\n' 'Note: All the hours are given in UTC.', - TextDumper(ephemerides, [], date=date(2019, 10, 14), with_colors=False).to_string()) + TextDumper(ephemerides, self._get_moon_phase(), [], date=date(2019, 10, 14), with_colors=False).to_string()) - ephemerides = self._get_data(aster_rise_set=True) + ephemerides = self._get_ephemerides(aster_rise_set=True) self.assertEqual('Monday October 14, 2019\n\n' 'Object Rise time Culmination time Set time\n' '-------- ----------- ------------------ ----------\n' @@ -108,10 +132,10 @@ class DumperTestCase(unittest.TestCase): 'Moon phase: Full Moon\n' 'Last Quarter on Monday October 21, 2019 at 00:00\n\n' 'Note: All the hours are given in UTC.', - TextDumper(ephemerides, [], date=date(2019, 10, 14), with_colors=False).to_string()) + TextDumper(ephemerides, self._get_moon_phase(), [], date=date(2019, 10, 14), with_colors=False).to_string()) def test_text_dumper_with_events(self): - ephemerides = self._get_data() + ephemerides = self._get_ephemerides() self.assertEqual("Monday October 14, 2019\n\n" "Object Rise time Culmination time Set time\n" "-------- ----------- ------------------ ----------\n" @@ -122,10 +146,9 @@ class DumperTestCase(unittest.TestCase): "23:00 Mars is in opposition\n" "12:00 Venus's largest elongation (42.0°)\n\n" "Note: All the hours are given in UTC.", - TextDumper(ephemerides, self._get_events(), date=date(2019, 10, 14), with_colors=False).to_string()) + TextDumper(ephemerides, self._get_moon_phase(), self._get_events(), date=date(2019, 10, 14), with_colors=False).to_string()) def test_text_dumper_without_ephemerides_and_with_events(self): - ephemerides = self._get_data(False) self.assertEqual('Monday October 14, 2019\n\n' 'Moon phase: Full Moon\n' 'Last Quarter on Monday October 21, 2019 at 00:00\n\n' @@ -133,10 +156,12 @@ class DumperTestCase(unittest.TestCase): '23:00 Mars is in opposition\n' "12:00 Venus's largest elongation (42.0°)\n\n" 'Note: All the hours are given in UTC.', - TextDumper(ephemerides, self._get_events(), date=date(2019, 10, 14), with_colors=False).to_string()) + TextDumper(None, self._get_moon_phase(), self._get_events(), + date=date(2019, 10, 14), with_colors=False).to_string()) def test_timezone_is_taken_in_account(self): - ephemerides = self._get_data(aster_rise_set=True) + ephemerides = self._get_ephemerides(aster_rise_set=True) + self.assertEqual('Monday October 14, 2019\n\n' 'Object Rise time Culmination time Set time\n' '-------- ----------- ------------------ -------------\n' @@ -147,9 +172,11 @@ class DumperTestCase(unittest.TestCase): 'Oct 15, 00:00 Mars is in opposition\n' "13:00 Venus's largest elongation (42.0°)\n\n" 'Note: All the hours are given in the UTC+1 timezone.', - TextDumper(ephemerides, self._get_events(), date=date(2019, 10, 14), with_colors=False, timezone=1).to_string()) + TextDumper(ephemerides, self._get_moon_phase(), self._get_events(), date=date(2019, 10, 14), + with_colors=False, timezone=1).to_string()) + + ephemerides = self._get_ephemerides(aster_rise_set=True) - ephemerides = self._get_data(aster_rise_set=True) self.assertEqual('Monday October 14, 2019\n\n' 'Object Rise time Culmination time Set time\n' '-------- ----------- ------------------ ----------\n' @@ -160,10 +187,13 @@ class DumperTestCase(unittest.TestCase): '22:00 Mars is in opposition\n' "11:00 Venus's largest elongation (42.0°)\n\n" 'Note: All the hours are given in the UTC-1 timezone.', - TextDumper(ephemerides, self._get_events(), date=date(2019, 10, 14), with_colors=False, timezone=-1).to_string()) + TextDumper(ephemerides, self._get_moon_phase(), self._get_events(), date=date(2019, 10, 14), + with_colors=False, timezone=-1).to_string()) def test_latex_dumper(self): - latex = _LatexDumper(self._get_data(), self._get_events(), date=date(2019, 10, 14)).to_string() + latex = _LatexDumper(self._get_ephemerides(), self._get_moon_phase(), self._get_events(), + date=date(2019, 10, 14)).to_string() + self.assertRegex(latex, 'Monday October 14, 2019') self.assertRegex(latex, 'Full Moon') self.assertRegex(latex, r'\\section{\\sffamily Expected events}') @@ -172,12 +202,14 @@ class DumperTestCase(unittest.TestCase): self.assertRegex(latex, r'\\event\{23:00\}\{Mars is in opposition\}') self.assertRegex(latex, r"\\event\{12:00\}\{Venus's largest elongation \(42.0°\)\}") - latex = _LatexDumper(self._get_data(aster_rise_set=True), + latex = _LatexDumper(self._get_ephemerides(aster_rise_set=True), self._get_moon_phase(), self._get_events(), date=date(2019, 10, 14)).to_string() self.assertRegex(latex, r'\\object\{Mars\}\{08:00\}\{13:00\}\{23:00\}') def test_latex_dumper_without_ephemerides(self): - latex = _LatexDumper(self._get_data(False), self._get_events(), date=date(2019, 10, 14)).to_string() + latex = _LatexDumper(None, self._get_moon_phase(), self._get_events(), + date=date(2019, 10, 14)).to_string() + self.assertRegex(latex, 'Monday October 14, 2019') self.assertRegex(latex, 'Full Moon') self.assertRegex(latex, r'\\section{\\sffamily Expected events}') @@ -188,7 +220,8 @@ class DumperTestCase(unittest.TestCase): self.assertNotRegex(latex, r'\\section{\\sffamily Ephemerides of the day}') def test_latex_dumper_without_events(self): - latex = _LatexDumper(self._get_data(), [], date=date(2019, 10, 14)).to_string() + latex = _LatexDumper(self._get_ephemerides(), self._get_moon_phase(), [], date=date(2019, 10, 14)).to_string() + self.assertRegex(latex, 'Monday October 14, 2019') self.assertRegex(latex, 'Full Moon') self.assertRegex(latex, r'\\object\{Mars\}\{-\}\{-\}\{-\}') @@ -197,16 +230,16 @@ class DumperTestCase(unittest.TestCase): self.assertNotRegex(latex, r'\\section{\\sffamily Expected events}') @staticmethod - def _get_data(has_ephemerides: bool = True, aster_rise_set=False): + def _get_ephemerides(aster_rise_set=False) -> [AsterEphemerides]: rise_time = datetime(2019, 10, 14, 8) if aster_rise_set else None culmination_time = datetime(2019, 10, 14, 13) if aster_rise_set else None set_time = datetime(2019, 10, 14, 23) if aster_rise_set else None - return { - 'moon_phase': MoonPhase('FULL_MOON', datetime(2019, 10, 14), datetime(2019, 10, 21)), - 'details': [Planet('Mars', 'MARS', - AsterEphemerides(rise_time, culmination_time, set_time))] if has_ephemerides else [] - } + return [AsterEphemerides(rise_time, culmination_time, set_time, Planet('Mars', 'MARS'))] + + @staticmethod + def _get_moon_phase(): + return MoonPhase('FULL_MOON', datetime(2019, 10, 14), datetime(2019, 10, 21)) @staticmethod def _get_events(): diff --git a/test/ephemerides.py b/test/ephemerides.py index 6d318f8..8eb81ad 100644 --- a/test/ephemerides.py +++ b/test/ephemerides.py @@ -1,106 +1,111 @@ import unittest -from kosmorrolib.ephemerides import EphemeridesComputer -from kosmorrolib.core import get_skf_objects -from kosmorrolib.data import Star, Position, MoonPhase +from .testutils import expect_assertions +from kosmorrolib import ephemerides +from kosmorrolib.data import EARTH, Position, MoonPhase from datetime import date -class EphemeridesComputerTestCase(unittest.TestCase): +class EphemeridesTestCase(unittest.TestCase): def test_get_ephemerides_for_aster_returns_correct_hours(self): - position = Position(0, 0) - position.observation_planet = get_skf_objects()['earth'] - star = EphemeridesComputer.get_asters_ephemerides_for_aster(Star('Sun', skyfield_name='sun'), - date=date(2019, 11, 18), - position=position) + position = Position(0, 0, EARTH) + eph = ephemerides.get_ephemerides(date=date(2019, 11, 18), + position=position) - self.assertRegex(star.ephemerides.rise_time.isoformat(), '^2019-11-18T05:41:') - self.assertRegex(star.ephemerides.culmination_time.isoformat(), '^2019-11-18T11:45:') - self.assertRegex(star.ephemerides.set_time.isoformat(), '^2019-11-18T17:48:') + @expect_assertions(self.assertRegex, num=3) + def do_assertions(assert_regex): + for ephemeris in eph: + if ephemeris.object.skyfield_name == 'SUN': + assert_regex(ephemeris.rise_time.isoformat(), '^2019-11-18T05:41:') + assert_regex(ephemeris.culmination_time.isoformat(), '^2019-11-18T11:45:') + assert_regex(ephemeris.set_time.isoformat(), '^2019-11-18T17:48:') + break + + do_assertions() ################################################################################################################### ### MOON PHASE TESTS ### ################################################################################################################### def test_moon_phase_new_moon(self): - phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 25)) + phase = ephemerides.get_moon_phase(date(2019, 11, 25)) self.assertEqual('WANING_CRESCENT', phase.identifier) self.assertIsNone(phase.time) self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-26T') - phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 26)) + phase = ephemerides.get_moon_phase(date(2019, 11, 26)) self.assertEqual('NEW_MOON', phase.identifier) self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-12-04T') - phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 27)) + phase = ephemerides.get_moon_phase(date(2019, 11, 27)) self.assertEqual('WAXING_CRESCENT', phase.identifier) self.assertIsNone(phase.time) self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-12-04T') def test_moon_phase_first_crescent(self): - phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 3)) + phase = ephemerides.get_moon_phase(date(2019, 11, 3)) self.assertEqual('WAXING_CRESCENT', phase.identifier) self.assertIsNone(phase.time) self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-04T') - phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 4)) + phase = ephemerides.get_moon_phase(date(2019, 11, 4)) self.assertEqual('FIRST_QUARTER', phase.identifier) self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-12T') - phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 5)) + phase = ephemerides.get_moon_phase(date(2019, 11, 5)) self.assertEqual('WAXING_GIBBOUS', phase.identifier) self.assertIsNone(phase.time) self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-12T') def test_moon_phase_full_moon(self): - phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 11)) + phase = ephemerides.get_moon_phase(date(2019, 11, 11)) self.assertEqual('WAXING_GIBBOUS', phase.identifier) self.assertIsNone(phase.time) self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-12T') - phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 12)) + phase = ephemerides.get_moon_phase(date(2019, 11, 12)) self.assertEqual('FULL_MOON', phase.identifier) self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-19T') - phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 13)) + phase = ephemerides.get_moon_phase(date(2019, 11, 13)) self.assertEqual('WANING_GIBBOUS', phase.identifier) self.assertIsNone(phase.time) self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-19T') def test_moon_phase_last_quarter(self): - phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 18)) + phase = ephemerides.get_moon_phase(date(2019, 11, 18)) self.assertEqual('WANING_GIBBOUS', phase.identifier) self.assertIsNone(phase.time) self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-19T') - phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 19)) + phase = ephemerides.get_moon_phase(date(2019, 11, 19)) self.assertEqual('LAST_QUARTER', phase.identifier) self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-26T') - phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 20)) + phase = ephemerides.get_moon_phase(date(2019, 11, 20)) self.assertEqual('WANING_CRESCENT', phase.identifier) self.assertIsNone(phase.time) self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-26T') def test_moon_phase_prediction(self): phase = MoonPhase('NEW_MOON', None, None) - self.assertEqual('First Quarter', phase.get_next_phase()) + self.assertEqual('First Quarter', phase.get_next_phase_name()) phase = MoonPhase('WAXING_CRESCENT', None, None) - self.assertEqual('First Quarter', phase.get_next_phase()) + self.assertEqual('First Quarter', phase.get_next_phase_name()) phase = MoonPhase('FIRST_QUARTER', None, None) - self.assertEqual('Full Moon', phase.get_next_phase()) + self.assertEqual('Full Moon', phase.get_next_phase_name()) phase = MoonPhase('WAXING_GIBBOUS', None, None) - self.assertEqual('Full Moon', phase.get_next_phase()) + self.assertEqual('Full Moon', phase.get_next_phase_name()) phase = MoonPhase('FULL_MOON', None, None) - self.assertEqual('Last Quarter', phase.get_next_phase()) + self.assertEqual('Last Quarter', phase.get_next_phase_name()) phase = MoonPhase('WANING_GIBBOUS', None, None) - self.assertEqual('Last Quarter', phase.get_next_phase()) + self.assertEqual('Last Quarter', phase.get_next_phase_name()) phase = MoonPhase('LAST_QUARTER', None, None) - self.assertEqual('New Moon', phase.get_next_phase()) + self.assertEqual('New Moon', phase.get_next_phase_name()) phase = MoonPhase('WANING_CRESCENT', None, None) - self.assertEqual('New Moon', phase.get_next_phase()) + self.assertEqual('New Moon', phase.get_next_phase_name()) if __name__ == '__main__': diff --git a/test/testutils.py b/test/testutils.py new file mode 100644 index 0000000..a1828eb --- /dev/null +++ b/test/testutils.py @@ -0,0 +1,48 @@ +import functools +from unittest import mock + + +def expect_assertions(assert_fun, num=1): + """Asserts that an assertion function is called as expected. + + This is very useful when the assertions are in loops. + To use it, create a nested function in the the test function. + The nested function will receive as parameter the mocked assertion function to use in place of the original one. + Finally, run the nested function. + + Example of use: + + >>> # the function we test: + >>> def my_sum_function(n, m): + >>> # some code here + >>> pass + + >>> # The unit test: + >>> def test_sum(self): + >>> @expect_assertions(self.assertEqual, num=10): + >>> def make_assertions(assert_equal): + >>> for i in range (-5, 5): + >>> for j in range(-5, 5): + >>> assert_equal(i + j, my_sum_function(i, j) + >>> + >>> make_assertions() # You don't need to give any parameter, the decorator does it for you. + + :param assert_fun: the assertion function to test + :param num: the number of times the assertion function is expected to be called + """ + assert_fun_mock = mock.Mock(side_effect=assert_fun) + + def fun_decorator(fun): + @functools.wraps(fun) + def sniff_function(): + fun(assert_fun_mock) + + count = assert_fun_mock.call_count + if count != num: + raise AssertionError('Expected %d call(s) to function %s but called %d time(s).' % (num, + assert_fun.__name__, + count)) + + return sniff_function + + return fun_decorator