Browse Source

Extract pygal french maps into a pygal_maps_fr plugin

pull/242/head
Florian Mounier 10 years ago
parent
commit
036e3b8b62
  1. 20
      demo/moulinrouge/tests.py
  2. 38
      pygal/__init__.py
  3. 6
      pygal/config.py
  4. 9
      pygal/graph/box.py
  5. 325
      pygal/graph/frenchmap.py
  6. 328
      pygal/graph/maps/fr.departments.svg
  7. 91
      pygal/graph/maps/fr.regions.svg
  8. 0
      pygal/maps/__init__.py
  9. 12
      pygal/test/test_box.py
  10. 24
      pygal/test/test_config.py
  11. 42
      pygal/test/test_frenchmap.py

20
demo/moulinrouge/tests.py

@ -3,14 +3,18 @@
from pygal import ( from pygal import (
Bar, Gauge, Pyramid, Funnel, Dot, StackedBar, StackedLine, XY, Bar, Gauge, Pyramid, Funnel, Dot, StackedBar, StackedLine, XY,
CHARTS_BY_NAME, Config, Line, Worldmap, Histogram, Box, SwissMapCantons, CHARTS_BY_NAME, Config, Line, Worldmap, Histogram, Box, SwissMapCantons,
FrenchMapDepartments, FrenchMapRegions, Pie, Treemap, TimeLine, DateLine, Pie, Treemap, TimeLine, DateLine,
DateTimeLine, SupranationalWorldmap) DateTimeLine, SupranationalWorldmap)
try:
from pygal.maps import fr
except ImportError:
fr = None
from flask import abort
from pygal.style import styles, Style, RotateStyle from pygal.style import styles, Style, RotateStyle
from pygal.colors import rotate from pygal.colors import rotate
from pygal.graph.horizontal import HorizontalGraph from pygal.graph.horizontal import HorizontalGraph
from pygal.graph.frenchmap import DEPARTMENTS, REGIONS
from pygal.graph.swissmap import CANTONS from pygal.graph.swissmap import CANTONS
from random import randint, choice from random import randint, choice
from datetime import datetime from datetime import datetime
@ -444,10 +448,12 @@ def get_test_routes(app):
@app.route('/test/frenchmapdepartments') @app.route('/test/frenchmapdepartments')
def test_frenchmapdepartments(): def test_frenchmapdepartments():
fmap = FrenchMapDepartments(style=choice(list(styles.values()))) if fr is None:
abort(404)
fmap = fr.Departments(style=choice(list(styles.values())))
for i in range(10): for i in range(10):
fmap.add('s%d' % i, [ fmap.add('s%d' % i, [
(choice(list(DEPARTMENTS.keys())), randint(0, 100)) (choice(list(fr.DEPARTMENTS.keys())), randint(0, 100))
for _ in range(randint(1, 5))]) for _ in range(randint(1, 5))])
fmap.add('links', [{ fmap.add('links', [{
@ -484,10 +490,12 @@ def get_test_routes(app):
@app.route('/test/frenchmapregions') @app.route('/test/frenchmapregions')
def test_frenchmapregions(): def test_frenchmapregions():
fmap = FrenchMapRegions(style=choice(list(styles.values()))) if fr is None:
abort(404)
fmap = fr.Regions(style=choice(list(styles.values())))
for i in range(10): for i in range(10):
fmap.add('s%d' % i, [ fmap.add('s%d' % i, [
(choice(list(REGIONS.keys())), randint(0, 100)) (choice(list(fr.REGIONS.keys())), randint(0, 100))
for _ in range(randint(1, 5))]) for _ in range(randint(1, 5))])
fmap.add('links', [{ fmap.add('links', [{

38
pygal/__init__.py

@ -26,7 +26,6 @@ __version__ = '1.9.9'
from pygal.graph.bar import Bar from pygal.graph.bar import Bar
from pygal.graph.box import Box from pygal.graph.box import Box
from pygal.graph.dot import Dot from pygal.graph.dot import Dot
from pygal.graph.frenchmap import FrenchMapDepartments, FrenchMapRegions
from pygal.graph.funnel import Funnel from pygal.graph.funnel import Funnel
from pygal.graph.gauge import Gauge from pygal.graph.gauge import Gauge
from pygal.graph.histogram import Histogram from pygal.graph.histogram import Histogram
@ -46,11 +45,48 @@ from pygal.graph.worldmap import Worldmap, SupranationalWorldmap
from pygal.graph.xy import XY from pygal.graph.xy import XY
from pygal.graph.graph import Graph from pygal.graph.graph import Graph
from pygal.config import Config from pygal.config import Config
from pygal import maps
import pkg_resources
import sys
import traceback
import warnings
CHARTS_BY_NAME = dict( CHARTS_BY_NAME = dict(
[(k, v) for k, v in locals().items() [(k, v) for k, v in locals().items()
if isinstance(v, type) and issubclass(v, Graph) and v != Graph]) if isinstance(v, type) and issubclass(v, Graph) and v != Graph])
from pygal.graph.map import BaseMap
for entry in pkg_resources.iter_entry_points('pygal.maps'):
try:
cls = entry.load()
except Exception:
warnings.warn('Unable to load %s pygal plugin \n\n%s' % (
entry, traceback.format_exc()), Warning)
continue
setattr(maps, entry.name, cls)
for k, v in cls.__dict__.items():
if isinstance(v, type) and issubclass(v, BaseMap) and v != BaseMap:
CHARTS_BY_NAME[entry.name.capitalize() + k + 'Map'] = v
CHARTS_NAMES = list(CHARTS_BY_NAME.keys()) CHARTS_NAMES = list(CHARTS_BY_NAME.keys())
CHARTS = list(CHARTS_BY_NAME.values()) CHARTS = list(CHARTS_BY_NAME.values())
class PluginImportFixer(object):
def __init__(self):
pass
def find_module(self, fullname, path=None):
if fullname.startswith('pygal.maps.') and hasattr(
maps, fullname.split('.')[2]):
return self
return None
def load_module(self, name):
if name not in sys.modules:
sys.modules[name] = getattr(maps, name.split('.')[2])
return sys.modules[name]
sys.meta_path += [PluginImportFixer()]

6
pygal/config.py

@ -342,10 +342,10 @@ class Config(CommonConfig):
"{'type': 'cardinal', 'c': .5}", int) "{'type': 'cardinal', 'c': .5}", int)
mode = Key( mode = Key(
None, str, "Value", "Sets the mode to be used. " 'extremes', str, "Value", "Sets the mode to be used. "
"(Currently only supported on box plot)", "(Currently only supported on box plot)",
"May be %s" % ' or '.join(["1.5IQR", "extremes", "tukey", "stdev",\ "May be %s" % ' or '.join([
"pstdev"])) "1.5IQR", "extremes", "tukey", "stdev", "pstdev"]))
order_min = Key( order_min = Key(
None, int, "Value", "Minimum order of scale, defaults to None") None, int, "Value", "Minimum order of scale, defaults to None")

9
pygal/graph/box.py

@ -54,7 +54,7 @@ class Box(Graph):
elif self.mode in ["tukey", "stdev", "pstdev"]: elif self.mode in ["tukey", "stdev", "pstdev"]:
return 'Min: %s Lower Whisker: %s Q1: %s Q2: %s Q3: %s '\ return 'Min: %s Lower Whisker: %s Q1: %s Q2: %s Q3: %s '\
'Upper Whisker: %s Max: %s' % tuple(map(sup, x)) 'Upper Whisker: %s Max: %s' % tuple(map(sup, x))
else: elif self.mode == '1.5IQR':
# 1.5IQR mode # 1.5IQR mode
return 'Q1: %s Q2: %s Q3: %s' % tuple(map(sup, x[2:5])) return 'Q1: %s Q2: %s Q3: %s' % tuple(map(sup, x[2:5]))
else: else:
@ -70,7 +70,6 @@ class Box(Graph):
serie.values, serie.outliers = \ serie.values, serie.outliers = \
self._box_points(serie.values, self.mode) self._box_points(serie.values, self.mode)
if self._min: if self._min:
self._box.ymin = min(self._min, self.zero) self._box.ymin = min(self._min, self.zero)
if self._max: if self._max:
@ -227,13 +226,13 @@ class Box(Graph):
m = mean(seq) m = mean(seq)
l = len(seq) l = len(seq)
v = sum((n - m)**2 for n in seq) / (l - 1) # variance v = sum((n - m)**2 for n in seq) / (l - 1) # variance
return v**0.5 # sqrt return v**0.5 # sqrt
def pstdev(seq): def pstdev(seq):
m = mean(seq) m = mean(seq)
l = len(seq) l = len(seq)
v = sum((n - m)**2 for n in seq) / l # variance v = sum((n - m)**2 for n in seq) / l # variance
return v**0.5 # sqrt return v**0.5 # sqrt
outliers = [] outliers = []
# sort the copy in case the originals must stay in original order # sort the copy in case the originals must stay in original order
@ -294,7 +293,7 @@ class Box(Graph):
q0 = s[b0] q0 = s[b0]
q4 = s[b4-1] q4 = s[b4-1]
outliers = s[:b0] + s[b4:] outliers = s[:b0] + s[b4:]
else: elif mode == '1.5IQR':
# 1.5IQR mode # 1.5IQR mode
q0 = q1 - 1.5 * iqr q0 = q1 - 1.5 * iqr
q4 = q3 + 1.5 * iqr q4 = q3 + 1.5 * iqr

325
pygal/graph/frenchmap.py

@ -1,325 +0,0 @@
# -*- coding: utf-8 -*-
# This file is part of pygal
#
# A python svg graph plotting library
# Copyright © 2012-2014 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""
Worldmap chart
"""
from __future__ import division
from collections import defaultdict
from pygal.graph.map import BaseMap
from pygal._compat import u
from numbers import Number
import os
DEPARTMENTS = {
'01': u("Ain"),
'02': u("Aisne"),
'03': u("Allier"),
'04': u("Alpes-de-Haute-Provence"),
'05': u("Hautes-Alpes"),
'06': u("Alpes-Maritimes"),
'07': u("Ardèche"),
'08': u("Ardennes"),
'09': u("Ariège"),
'10': u("Aube"),
'11': u("Aude"),
'12': u("Aveyron"),
'13': u("Bouches-du-Rhône"),
'14': u("Calvados"),
'15': u("Cantal"),
'16': u("Charente"),
'17': u("Charente-Maritime"),
'18': u("Cher"),
'19': u("Corrèze"),
'2A': u("Corse-du-Sud"),
'2B': u("Haute-Corse"),
'21': u("Côte-d'Or"),
'22': u("Côtes-d'Armor"),
'23': u("Creuse"),
'24': u("Dordogne"),
'25': u("Doubs"),
'26': u("Drôme"),
'27': u("Eure"),
'28': u("Eure-et-Loir"),
'29': u("Finistère"),
'30': u("Gard"),
'31': u("Haute-Garonne"),
'32': u("Gers"),
'33': u("Gironde"),
'34': u("Hérault"),
'35': u("Ille-et-Vilaine"),
'36': u("Indre"),
'37': u("Indre-et-Loire"),
'38': u("Isère"),
'39': u("Jura"),
'40': u("Landes"),
'41': u("Loir-et-Cher"),
'42': u("Loire"),
'43': u("Haute-Loire"),
'44': u("Loire-Atlantique"),
'45': u("Loiret"),
'46': u("Lot"),
'47': u("Lot-et-Garonne"),
'48': u("Lozère"),
'49': u("Maine-et-Loire"),
'50': u("Manche"),
'51': u("Marne"),
'52': u("Haute-Marne"),
'53': u("Mayenne"),
'54': u("Meurthe-et-Moselle"),
'55': u("Meuse"),
'56': u("Morbihan"),
'57': u("Moselle"),
'58': u("Nièvre"),
'59': u("Nord"),
'60': u("Oise"),
'61': u("Orne"),
'62': u("Pas-de-Calais"),
'63': u("Puy-de-Dôme"),
'64': u("Pyrénées-Atlantiques"),
'65': u("Hautes-Pyrénées"),
'66': u("Pyrénées-Orientales"),
'67': u("Bas-Rhin"),
'68': u("Haut-Rhin"),
'69': u("Rhône"),
'70': u("Haute-Saône"),
'71': u("Saône-et-Loire"),
'72': u("Sarthe"),
'73': u("Savoie"),
'74': u("Haute-Savoie"),
'75': u("Paris"),
'76': u("Seine-Maritime"),
'77': u("Seine-et-Marne"),
'78': u("Yvelines"),
'79': u("Deux-Sèvres"),
'80': u("Somme"),
'81': u("Tarn"),
'82': u("Tarn-et-Garonne"),
'83': u("Var"),
'84': u("Vaucluse"),
'85': u("Vendée"),
'86': u("Vienne"),
'87': u("Haute-Vienne"),
'88': u("Vosges"),
'89': u("Yonne"),
'90': u("Territoire de Belfort"),
'91': u("Essonne"),
'92': u("Hauts-de-Seine"),
'93': u("Seine-Saint-Denis"),
'94': u("Val-de-Marne"),
'95': u("Val-d'Oise"),
'971': u("Guadeloupe"),
'972': u("Martinique"),
'973': u("Guyane"),
'974': u("Réunion"),
# Not a area anymore but in case of...
'975': u("Saint Pierre et Miquelon"),
'976': u("Mayotte")
}
REGIONS = {
'11': u("Île-de-France"),
'21': u("Champagne-Ardenne"),
'22': u("Picardie"),
'23': u("Haute-Normandie"),
'24': u("Centre"),
'25': u("Basse-Normandie"),
'26': u("Bourgogne"),
'31': u("Nord-Pas-de-Calais"),
'41': u("Lorraine"),
'42': u("Alsace"),
'43': u("Franche-Comté"),
'52': u("Pays-de-la-Loire"),
'53': u("Bretagne"),
'54': u("Poitou-Charentes"),
'72': u("Aquitaine"),
'73': u("Midi-Pyrénées"),
'74': u("Limousin"),
'82': u("Rhône-Alpes"),
'83': u("Auvergne"),
'91': u("Languedoc-Roussillon"),
'93': u("Provence-Alpes-Côte d'Azur"),
'94': u("Corse"),
'01': u("Guadeloupe"),
'02': u("Martinique"),
'03': u("Guyane"),
'04': u("Réunion"),
# Not a region anymore but in case of...
'05': u("Saint Pierre et Miquelon"),
'06': u("Mayotte")
}
with open(os.path.join(
os.path.dirname(__file__), 'maps',
'fr.departments.svg')) as file:
DPT_MAP = file.read()
class IntCodeMixin(object):
def adapt_code(self, area_code):
if isinstance(area_code, Number):
if area_code > 100:
return '%3d' % area_code
return '%2d' % area_code
return super(IntCodeMixin, self).adapt_code(area_code)
class FrenchMapDepartments(IntCodeMixin, BaseMap):
"""French department map"""
x_labels = list(DEPARTMENTS.keys())
area_names = DEPARTMENTS
area_prefix = 'z'
kind = 'departement'
svg_map = DPT_MAP
with open(os.path.join(
os.path.dirname(__file__), 'maps',
'fr.regions.svg')) as file:
REG_MAP = file.read()
class FrenchMapRegions(IntCodeMixin, BaseMap):
"""French regions map"""
x_labels = list(REGIONS.keys())
area_names = REGIONS
area_prefix = 'a'
svg_map = REG_MAP
kind = 'region'
DEPARTMENTS_REGIONS = {
"01": "82",
"02": "22",
"03": "83",
"04": "93",
"05": "93",
"06": "93",
"07": "82",
"08": "21",
"09": "73",
"10": "21",
"11": "91",
"12": "73",
"13": "93",
"14": "25",
"15": "83",
"16": "54",
"17": "54",
"18": "24",
"19": "74",
"21": "26",
"22": "53",
"23": "74",
"24": "72",
"25": "43",
"26": "82",
"27": "23",
"28": "24",
"29": "53",
"2A": "94",
"2B": "94",
"30": "91",
"31": "73",
"32": "73",
"33": "72",
"34": "91",
"35": "53",
"36": "24",
"37": "24",
"38": "82",
"39": "43",
"40": "72",
"41": "24",
"42": "82",
"43": "83",
"44": "52",
"45": "24",
"46": "73",
"47": "72",
"48": "91",
"49": "52",
"50": "25",
"51": "21",
"52": "21",
"53": "52",
"54": "41",
"55": "41",
"56": "53",
"57": "41",
"58": "26",
"59": "31",
"60": "22",
"61": "25",
"62": "31",
"63": "83",
"64": "72",
"65": "73",
"66": "91",
"67": "42",
"68": "42",
"69": "82",
"70": "43",
"71": "26",
"72": "52",
"73": "82",
"74": "82",
"75": "11",
"76": "23",
"77": "11",
"78": "11",
"79": "54",
"80": "22",
"81": "73",
"82": "73",
"83": "93",
"84": "93",
"85": "52",
"86": "54",
"87": "74",
"88": "41",
"89": "26",
"90": "43",
"91": "11",
"92": "11",
"93": "11",
"94": "11",
"95": "11",
"971": "01",
"972": "02",
"973": "03",
"974": "04",
"975": "05",
"976": "06"
}
def aggregate_regions(values):
if isinstance(values, dict):
values = values.items()
regions = defaultdict(int)
for department, value in values:
regions[DEPARTMENTS_REGIONS[department]] += value
return list(regions.items())

328
pygal/graph/maps/fr.departments.svg

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 583 KiB

91
pygal/graph/maps/fr.regions.svg

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 370 KiB

0
pygal/maps/__init__.py

12
pygal/test/test_box.py

@ -22,7 +22,8 @@ from pygal import Box as ghostedBox
def test_quartiles(): def test_quartiles():
a = [-2.0, 3.0, 4.0, 5.0, 8.0] # odd test data a = [-2.0, 3.0, 4.0, 5.0, 8.0] # odd test data
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(a) (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
a, mode='1.5IQR')
assert q1 == 7.0 / 4.0 assert q1 == 7.0 / 4.0
assert q2 == 4.0 assert q2 == 4.0
@ -31,17 +32,20 @@ def test_quartiles():
assert q4 == 23 / 4.0 + 6.0 # q3 + 1.5 * iqr assert q4 == 23 / 4.0 + 6.0 # q3 + 1.5 * iqr
b = [1.0, 4.0, 6.0, 8.0] # even test data b = [1.0, 4.0, 6.0, 8.0] # even test data
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(b) (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
b, mode='1.5IQR')
assert q2 == 5.0 assert q2 == 5.0
c = [2.0, None, 4.0, 6.0, None] # odd with None elements c = [2.0, None, 4.0, 6.0, None] # odd with None elements
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(c) (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
c, mode='1.5IQR')
assert q2 == 4.0 assert q2 == 4.0
d = [4] d = [4]
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(d) (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
d, mode='1.5IQR')
assert q0 == 4 assert q0 == 4
assert q1 == 4 assert q1 == 4

24
pygal/test/test_config.py

@ -20,9 +20,9 @@
from pygal import ( from pygal import (
Line, Dot, Pie, Treemap, Radar, Config, Bar, Funnel, Worldmap, Line, Dot, Pie, Treemap, Radar, Config, Bar, Funnel, Worldmap,
SupranationalWorldmap, Histogram, Gauge, Box, XY, SupranationalWorldmap, Histogram, Gauge, Box, XY,
Pyramid, HorizontalBar, HorizontalStackedBar, Pyramid, HorizontalBar, HorizontalStackedBar, SwissMapCantons,
FrenchMapRegions, FrenchMapDepartments, SwissMapCantons,
DateTimeLine, TimeLine, DateLine, TimeDeltaLine) DateTimeLine, TimeLine, DateLine, TimeDeltaLine)
from pygal.graph.map import BaseMap
from pygal._compat import u from pygal._compat import u
from pygal.test.utils import texts from pygal.test.utils import texts
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
@ -273,9 +273,9 @@ def test_no_data():
def test_include_x_axis(Chart): def test_include_x_axis(Chart):
chart = Chart() chart = Chart()
if Chart in (Pie, Treemap, Radar, Funnel, Dot, Gauge, Worldmap, if Chart in (
SupranationalWorldmap, Histogram, Box, Pie, Treemap, Radar, Funnel, Dot, Gauge, Histogram, Box
FrenchMapRegions, FrenchMapDepartments, SwissMapCantons): ) or issubclass(Chart, BaseMap):
return return
if not chart._dual: if not chart._dual:
data = 100, 200, 150 data = 100, 200, 150
@ -360,11 +360,10 @@ def test_x_y_title(Chart):
def test_x_label_major(Chart): def test_x_label_major(Chart):
if Chart in ( if Chart in (
Pie, Treemap, Funnel, Dot, Gauge, Worldmap, Pie, Treemap, Funnel, Dot, Gauge, Histogram, Box,
SupranationalWorldmap, Histogram, Box,
FrenchMapRegions, FrenchMapDepartments, SwissMapCantons,
Pyramid, DateTimeLine, TimeLine, DateLine, Pyramid, DateTimeLine, TimeLine, DateLine,
TimeDeltaLine): TimeDeltaLine
) or issubclass(Chart, BaseMap):
return return
chart = Chart() chart = Chart()
chart.add('test', range(12)) chart.add('test', range(12))
@ -405,12 +404,11 @@ def test_x_label_major(Chart):
def test_y_label_major(Chart): def test_y_label_major(Chart):
if Chart in ( if Chart in (
Pie, Treemap, Funnel, Dot, Gauge, Worldmap, Pie, Treemap, Funnel, Dot, Gauge, Histogram, Box,
SupranationalWorldmap, Histogram, Box,
FrenchMapRegions, FrenchMapDepartments, SwissMapCantons,
HorizontalBar, HorizontalStackedBar, HorizontalBar, HorizontalStackedBar,
Pyramid, DateTimeLine, TimeLine, DateLine, Pyramid, DateTimeLine, TimeLine, DateLine,
TimeDeltaLine): TimeDeltaLine
) or issubclass(Chart, BaseMap):
return return
chart = Chart() chart = Chart()
data = range(12) data = range(12)

42
pygal/test/test_frenchmap.py

@ -1,42 +0,0 @@
# -*- coding: utf-8 -*-
# This file is part of pygal
#
# A python svg graph plotting library
# Copyright © 2012-2014 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
from pygal import (
FrenchMapRegions, FrenchMapDepartments)
from pygal.graph.frenchmap import REGIONS, DEPARTMENTS, aggregate_regions
def test_frenchmaps():
datas = {}
for dept in DEPARTMENTS.keys():
datas[dept] = int(''.join([x for x in dept if x.isdigit()])) * 10
fmap = FrenchMapDepartments()
fmap.add('departements', datas)
q = fmap.render_pyquery()
assert len(
q('#departements .departement,#dom-com .departement')
) == len(DEPARTMENTS)
fmap = FrenchMapRegions()
fmap.add('regions', aggregate_regions(datas))
q = fmap.render_pyquery()
assert len(q('#regions .region,#dom-com .region')) == len(REGIONS)
assert aggregate_regions(datas.items()) == aggregate_regions(datas)
Loading…
Cancel
Save