diff --git a/docs/working_with_kml.rst b/docs/working_with_kml.rst index 3a41129d..172ad5bc 100644 --- a/docs/working_with_kml.rst +++ b/docs/working_with_kml.rst @@ -195,9 +195,6 @@ Now we can remove the CascadingStyle from the document and have a look at the re - - hide - 1.2 @@ -211,11 +208,11 @@ Now we can remove the CascadingStyle from the document and have a look at the re 80000000 - - hide + + https://earth.google.com/earth/rpc/cc/icon?color=1976d2&id=2000&scale=4 @@ -228,6 +225,9 @@ Now we can remove the CascadingStyle from the document and have a look at the re 80000000 + + hide + Ort1 diff --git a/fastkml/features.py b/fastkml/features.py index 08e6ed24..1a204ad8 100644 --- a/fastkml/features.py +++ b/fastkml/features.py @@ -31,6 +31,7 @@ from pygeoif.types import GeoType from fastkml import atom +from fastkml import config from fastkml import gx from fastkml.base import _XMLObject from fastkml.data import ExtendedData @@ -97,6 +98,8 @@ class Snippet(_XMLObject): maximum number of lines to display. """ + _default_nsid = config.KML + text: Optional[str] max_lines: Optional[int] = None @@ -109,7 +112,7 @@ def __init__( **kwargs: Any, ) -> None: """ - Initialize a Feature object. + Initialize a Snippet object. Args: ---- @@ -130,7 +133,7 @@ def __init__( """ super().__init__(ns=ns, name_spaces=name_spaces, **kwargs) - self.text = text + self.text = text.strip() or None if text else None self.max_lines = max_lines def __repr__(self) -> str: @@ -168,9 +171,9 @@ def __bool__(self) -> bool: registry.register( Snippet, RegistryItem( - ns_ids=("kml",), + ns_ids=("kml", ""), attr_name="text", - node_name="", + node_name="Snippet", classes=(str,), get_kwarg=node_text_kwarg, set_element=node_text, @@ -284,7 +287,7 @@ def __init__( **kwargs, ) self.name = name - self.description = description + self.description = description.strip() or None if description else None self.style_url = style_url self.styles = list(styles) if styles else [] self.view = view diff --git a/fastkml/styles.py b/fastkml/styles.py index 79c08aa5..763f0888 100644 --- a/fastkml/styles.py +++ b/fastkml/styles.py @@ -1165,11 +1165,11 @@ def __bool__(self) -> bool: attr_name="styles", node_name="Style", classes=( - BalloonStyle, IconStyle, LabelStyle, LineStyle, PolyStyle, + BalloonStyle, ), get_kwarg=xml_subelement_list_kwarg, set_element=xml_subelement_list, @@ -1284,7 +1284,7 @@ def __bool__(self) -> bool: RegistryItem( ns_ids=("kml",), attr_name="style", - node_name="style", + node_name="Style", classes=( StyleUrl, Style, diff --git a/fastkml/validator.py b/fastkml/validator.py index c40fdf82..5c1f8c8e 100644 --- a/fastkml/validator.py +++ b/fastkml/validator.py @@ -14,6 +14,7 @@ # along with this library; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """Validate KML files against the XML schema.""" + import logging import pathlib from functools import lru_cache @@ -98,6 +99,8 @@ def validate( ].getparent() except config.etree.XPathEvalError: parent = element + if parent is None: + parent = element error_in_xml = config.etree.tostring( parent, encoding="UTF-8", diff --git a/tests/hypothesis/common.py b/tests/hypothesis/common.py index 6d09ba40..639676ff 100644 --- a/tests/hypothesis/common.py +++ b/tests/hypothesis/common.py @@ -36,6 +36,7 @@ from fastkml.enums import DataType from fastkml.enums import DateTimeResolution from fastkml.enums import DisplayMode +from fastkml.enums import PairKey from fastkml.enums import RefreshMode from fastkml.enums import Units from fastkml.enums import Verbosity @@ -67,6 +68,7 @@ "Units": Units, "ColorMode": ColorMode, "DisplayMode": DisplayMode, + "PairKey": PairKey, "tzutc": tzutc, "tzfile": tzfile, } diff --git a/tests/hypothesis/data_test.py b/tests/hypothesis/data_test.py index 37882f8c..0d6d75c0 100644 --- a/tests/hypothesis/data_test.py +++ b/tests/hypothesis/data_test.py @@ -201,9 +201,11 @@ def test_fuzz_extended_data( ], ) -> None: extended_data = fastkml.ExtendedData( - elements=sorted(elements, key=lambda t: t.__class__.__name__) - if elements - else None, + elements=( + sorted(elements, key=lambda t: t.__class__.__name__) + if elements + else None + ), ) assert_repr_roundtrip(extended_data) diff --git a/tests/hypothesis/gx_test.py b/tests/hypothesis/gx_test.py index 823aae4e..8d0a01c7 100644 --- a/tests/hypothesis/gx_test.py +++ b/tests/hypothesis/gx_test.py @@ -14,6 +14,7 @@ # along with this library; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """Test gx Track and MultiTrack.""" + import typing from hypothesis import given @@ -37,16 +38,11 @@ track_items = st.builds( TrackItem, - angle=st.one_of( - st.one_of( - st.builds(Angle), - st.builds( - Angle, - heading=st.floats(allow_nan=False, allow_infinity=False), - roll=st.floats(allow_nan=False, allow_infinity=False), - tilt=st.floats(allow_nan=False, allow_infinity=False), - ), - ), + angle=st.builds( + Angle, + heading=st.floats(allow_nan=False, allow_infinity=False), + roll=st.floats(allow_nan=False, allow_infinity=False), + tilt=st.floats(allow_nan=False, allow_infinity=False), ), coord=points(srs=epsg4326), when=kml_datetimes(), @@ -54,7 +50,6 @@ class TestGx(Lxml): - @given( id=st.one_of(st.none(), nc_name()), target_id=st.one_of(st.none(), nc_name()), diff --git a/tests/hypothesis/strategies.py b/tests/hypothesis/strategies.py index e987ec2b..0bedf8b0 100644 --- a/tests/hypothesis/strategies.py +++ b/tests/hypothesis/strategies.py @@ -25,9 +25,14 @@ from hypothesis import strategies as st from hypothesis.extra.dateutil import timezones +from pygeoif.hypothesis import strategies as geo_st import fastkml.enums +import fastkml.links +import fastkml.styles from fastkml.times import KmlDateTime +from fastkml.views import LatLonAltBox +from fastkml.views import Lod ID_TEXT: Final = string.ascii_letters + string.digits + ".-_" @@ -85,6 +90,80 @@ ), ) +geometries = partial( + st.one_of, + ( + geo_st.points(srs=geo_st.epsg4326), + geo_st.line_strings(srs=geo_st.epsg4326), + geo_st.linear_rings(srs=geo_st.epsg4326), + geo_st.polygons(srs=geo_st.epsg4326), + geo_st.multi_points(srs=geo_st.epsg4326), + geo_st.multi_line_strings(srs=geo_st.epsg4326), + geo_st.multi_polygons(srs=geo_st.epsg4326), + ), +) +lods = partial( + st.builds, + Lod, + min_lod_pixels=st.integers(), + max_lod_pixels=st.integers(), + min_fade_extent=st.integers(), + max_fade_extent=st.integers(), +) + +lat_lon_alt_boxes = partial( + st.builds, + LatLonAltBox, + north=st.floats(allow_nan=False, allow_infinity=False, min_value=0, max_value=90), + south=st.floats(allow_nan=False, allow_infinity=False, min_value=0, max_value=90), + east=st.floats(allow_nan=False, allow_infinity=False, min_value=0, max_value=180), + west=st.floats(allow_nan=False, allow_infinity=False, min_value=0, max_value=180), + min_altitude=st.floats(allow_nan=False, allow_infinity=False).filter( + lambda x: x != 0, + ), + max_altitude=st.floats(allow_nan=False, allow_infinity=False).filter( + lambda x: x != 0, + ), + altitude_mode=st.sampled_from(fastkml.enums.AltitudeMode), +) + +kml_colors = partial( + st.text, + alphabet=string.hexdigits, + min_size=8, + max_size=8, +) + +styles = partial( + st.one_of, + st.builds( + fastkml.styles.LabelStyle, + color=kml_colors(), + color_mode=st.sampled_from(fastkml.enums.ColorMode), + scale=st.floats(allow_nan=False, allow_infinity=False), + ), + st.builds( + fastkml.styles.LineStyle, + color=kml_colors(), + color_mode=st.sampled_from(fastkml.enums.ColorMode), + width=st.floats(allow_nan=False, allow_infinity=False, min_value=0), + ), + st.builds( + fastkml.styles.PolyStyle, + color=kml_colors(), + color_mode=st.sampled_from(fastkml.enums.ColorMode), + fill=st.booleans(), + outline=st.booleans(), + ), + st.builds( + fastkml.styles.BalloonStyle, + bg_color=kml_colors(), + text_color=kml_colors(), + text=xml_text(min_size=1, max_size=256).filter(lambda x: x.strip() != ""), + display_mode=st.sampled_from(fastkml.enums.DisplayMode), + ), +) + @st.composite def query_strings(draw: st.DrawFn) -> str: @@ -95,14 +174,3 @@ def query_strings(draw: st.DrawFn) -> str: ), ) return urlencode(params) - - -@st.composite -def kml_colors(draw: st.DrawFn) -> str: - return draw( - st.text( - alphabet=string.hexdigits, - min_size=8, - max_size=8, - ), - ) diff --git a/tests/hypothesis/style_test.py b/tests/hypothesis/style_test.py index 7e9779fe..c70ba7e2 100644 --- a/tests/hypothesis/style_test.py +++ b/tests/hypothesis/style_test.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -"""Property-based tests for the views module.""" +"""Property-based tests for the styles module.""" import typing @@ -32,6 +32,7 @@ from tests.hypothesis.common import assert_str_roundtrip_verbose from tests.hypothesis.strategies import kml_colors from tests.hypothesis.strategies import nc_name +from tests.hypothesis.strategies import styles from tests.hypothesis.strategies import xml_text @@ -265,3 +266,263 @@ def test_fuzz_balloon_style( assert_str_roundtrip(balloon_style) assert_str_roundtrip_terse(balloon_style) assert_str_roundtrip_verbose(balloon_style) + + @given( + id=st.one_of(st.none(), nc_name()), + target_id=st.one_of(st.none(), nc_name()), + styles=st.one_of( + st.none(), + st.tuples( + st.builds( + fastkml.styles.IconStyle, + color=kml_colors(), + color_mode=st.sampled_from(fastkml.enums.ColorMode), + scale=st.floats(allow_nan=False, allow_infinity=False), + heading=st.floats( + allow_nan=False, + allow_infinity=False, + min_value=0, + max_value=360, + ), + icon=st.builds(fastkml.links.Icon, href=urls()), + ), + st.builds( + fastkml.styles.LabelStyle, + color=kml_colors(), + color_mode=st.sampled_from(fastkml.enums.ColorMode), + scale=st.floats(allow_nan=False, allow_infinity=False), + ), + st.builds( + fastkml.styles.LineStyle, + color=kml_colors(), + color_mode=st.sampled_from(fastkml.enums.ColorMode), + width=st.floats(allow_nan=False, allow_infinity=False, min_value=0), + ), + st.builds( + fastkml.styles.PolyStyle, + color=kml_colors(), + color_mode=st.sampled_from(fastkml.enums.ColorMode), + fill=st.booleans(), + outline=st.booleans(), + ), + st.builds( + fastkml.styles.BalloonStyle, + bg_color=kml_colors(), + text_color=kml_colors(), + text=xml_text(min_size=1, max_size=256).filter( + lambda x: x.strip() != "", + ), + display_mode=st.sampled_from(fastkml.enums.DisplayMode), + ), + ), + ), + ) + def test_fuzz_style( + self, + id: typing.Optional[str], + target_id: typing.Optional[str], + styles: typing.Optional[ + typing.Iterable[ + typing.Union[ + fastkml.BalloonStyle, + fastkml.IconStyle, + fastkml.LabelStyle, + fastkml.LineStyle, + fastkml.PolyStyle, + ] + ] + ], + ) -> None: + style = fastkml.Style(id=id, target_id=target_id, styles=styles) + + assert_repr_roundtrip(style) + assert_str_roundtrip(style) + assert_str_roundtrip_terse(style) + # assert_str_roundtrip_verbose disabled because of IconStyle + + @given( + id=st.one_of(st.none(), nc_name()), + target_id=st.one_of(st.none(), nc_name()), + styles=st.one_of( + st.none(), + st.tuples( + st.builds( + fastkml.styles.LabelStyle, + color=kml_colors(), + color_mode=st.sampled_from(fastkml.enums.ColorMode), + scale=st.floats(allow_nan=False, allow_infinity=False), + ), + st.builds( + fastkml.styles.LineStyle, + color=kml_colors(), + color_mode=st.sampled_from(fastkml.enums.ColorMode), + width=st.floats(allow_nan=False, allow_infinity=False, min_value=0), + ), + st.builds( + fastkml.styles.PolyStyle, + color=kml_colors(), + color_mode=st.sampled_from(fastkml.enums.ColorMode), + fill=st.booleans(), + outline=st.booleans(), + ), + st.builds( + fastkml.styles.BalloonStyle, + bg_color=kml_colors(), + text_color=kml_colors(), + text=xml_text(min_size=1, max_size=256).filter( + lambda x: x.strip() != "", + ), + display_mode=st.sampled_from(fastkml.enums.DisplayMode), + ), + ), + ), + ) + def test_fuzz_styles_no_icon_style( + self, + id: typing.Optional[str], + target_id: typing.Optional[str], + styles: typing.Optional[ + typing.Iterable[ + typing.Union[ + fastkml.BalloonStyle, + fastkml.LabelStyle, + fastkml.LineStyle, + fastkml.PolyStyle, + ] + ] + ], + ) -> None: + style = fastkml.Style(id=id, target_id=target_id, styles=styles) + + assert_repr_roundtrip(style) + assert_str_roundtrip(style) + assert_str_roundtrip_terse(style) + assert_str_roundtrip_verbose(style) + + @given( + id=st.one_of(st.none(), nc_name()), + target_id=st.one_of(st.none(), nc_name()), + key=st.sampled_from(fastkml.enums.PairKey), + style=st.one_of( + st.builds( + fastkml.StyleUrl, + url=urls(), + ), + st.builds( + fastkml.Style, + styles=st.lists( + styles(), + min_size=1, + max_size=1, + ), + ), + ), + ) + def test_fuzz_pair( + self, + id: typing.Optional[str], + target_id: typing.Optional[str], + key: typing.Optional[fastkml.enums.PairKey], + style: typing.Union[fastkml.StyleUrl, fastkml.Style, None], + ) -> None: + pair = fastkml.styles.Pair(id=id, target_id=target_id, key=key, style=style) + + assert_repr_roundtrip(pair) + assert_str_roundtrip(pair) + assert_str_roundtrip_terse(pair) + assert_str_roundtrip_verbose(pair) + + @given( + id=st.one_of(st.none(), nc_name()), + target_id=st.one_of(st.none(), nc_name()), + pairs=st.one_of( + st.none(), + st.lists( + st.builds( + fastkml.styles.Pair, + key=st.sampled_from(fastkml.enums.PairKey), + style=st.one_of( + st.builds( + fastkml.StyleUrl, + url=urls(), + ), + st.builds( + fastkml.Style, + styles=st.lists( + styles(), + min_size=1, + max_size=1, + ), + ), + ), + ), + ), + ), + ) + def test_fuzz_style_map_one_pair( + self, + id: typing.Optional[str], + target_id: typing.Optional[str], + pairs: typing.Optional[typing.Tuple[fastkml.styles.Pair]], + ) -> None: + style_map = fastkml.StyleMap(id=id, target_id=target_id, pairs=pairs) + + assert_repr_roundtrip(style_map) + assert_str_roundtrip(style_map) + assert_str_roundtrip_terse(style_map) + assert_str_roundtrip_verbose(style_map) + + @given( + id=st.one_of(st.none(), nc_name()), + target_id=st.one_of(st.none(), nc_name()), + pairs=st.tuples( + st.builds( + fastkml.styles.Pair, + key=st.just(fastkml.enums.PairKey.normal), + style=st.one_of( + st.builds( + fastkml.StyleUrl, + url=urls(), + ), + st.builds( + fastkml.Style, + styles=st.lists( + styles(), + min_size=1, + max_size=1, + ), + ), + ), + ), + st.builds( + fastkml.styles.Pair, + key=st.just(fastkml.enums.PairKey.highlight), + style=st.one_of( + st.builds( + fastkml.StyleUrl, + url=urls(), + ), + st.builds( + fastkml.Style, + styles=st.lists( + styles(), + min_size=1, + max_size=1, + ), + ), + ), + ), + ), + ) + def test_fuzz_style_map_pairs( + self, + id: typing.Optional[str], + target_id: typing.Optional[str], + pairs: typing.Optional[typing.Tuple[fastkml.styles.Pair]], + ) -> None: + style_map = fastkml.StyleMap(id=id, target_id=target_id, pairs=pairs) + + assert_repr_roundtrip(style_map) + assert_str_roundtrip(style_map) + assert_str_roundtrip_terse(style_map) + assert_str_roundtrip_verbose(style_map) diff --git a/tests/hypothesis/views_test.py b/tests/hypothesis/views_test.py index bfdc8f8a..5cd92d78 100644 --- a/tests/hypothesis/views_test.py +++ b/tests/hypothesis/views_test.py @@ -14,6 +14,7 @@ # along with this library; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """Property-based tests for the views module.""" + import typing from functools import partial @@ -23,40 +24,15 @@ import fastkml import fastkml.enums import fastkml.views -from fastkml.views import LatLonAltBox -from fastkml.views import Lod from tests.base import Lxml from tests.hypothesis.common import assert_repr_roundtrip from tests.hypothesis.common import assert_str_roundtrip from tests.hypothesis.common import assert_str_roundtrip_terse from tests.hypothesis.common import assert_str_roundtrip_verbose +from tests.hypothesis.strategies import lat_lon_alt_boxes +from tests.hypothesis.strategies import lods from tests.hypothesis.strategies import nc_name -lods = partial( - st.builds, - Lod, - min_lod_pixels=st.integers(), - max_lod_pixels=st.integers(), - min_fade_extent=st.integers(), - max_fade_extent=st.integers(), -) - -lat_lon_alt_boxes = partial( - st.builds, - LatLonAltBox, - north=st.floats(allow_nan=False, allow_infinity=False, min_value=0, max_value=90), - south=st.floats(allow_nan=False, allow_infinity=False, min_value=0, max_value=90), - east=st.floats(allow_nan=False, allow_infinity=False, min_value=0, max_value=180), - west=st.floats(allow_nan=False, allow_infinity=False, min_value=0, max_value=180), - min_altitude=st.floats(allow_nan=False, allow_infinity=False).filter( - lambda x: x != 0, - ), - max_altitude=st.floats(allow_nan=False, allow_infinity=False).filter( - lambda x: x != 0, - ), - altitude_mode=st.sampled_from(fastkml.enums.AltitudeMode), -) - common_view = partial( given, id=st.one_of(st.none(), nc_name()), @@ -96,7 +72,6 @@ class TestLxml(Lxml): - @given( min_lod_pixels=st.one_of(st.none(), st.integers()), max_lod_pixels=st.one_of(st.none(), st.integers()), diff --git a/tests/styles_test.py b/tests/styles_test.py index 7677dcaa..3dc43252 100644 --- a/tests/styles_test.py +++ b/tests/styles_test.py @@ -412,44 +412,45 @@ def test_style_read(self) -> None: assert style.id == "id-0" assert style.target_id == "target-0" - assert isinstance(style.styles[0], styles.BalloonStyle) - assert style.styles[0].id == "id-b0" - assert style.styles[0].target_id == "target-b0" - assert style.styles[0].bg_color == "7fff0000" - assert style.styles[0].text_color == "ff00ff00" - assert style.styles[0].text == "Hello" - assert style.styles[0].display_mode == DisplayMode.hide - - assert isinstance(style.styles[1], styles.IconStyle) - assert style.styles[1].id == "id-i0" - assert style.styles[1].target_id == "target-i0" + + assert isinstance(style.styles[0], styles.IconStyle) + assert style.styles[0].id == "id-i0" + assert style.styles[0].target_id == "target-i0" + assert style.styles[0].color == "ff0000ff" + assert style.styles[0].color_mode == ColorMode.random + assert style.styles[0].scale == 1.0 + assert style.styles[0].heading == 0 + assert style.styles[0].icon.href == "http://example.com/icon.png" + + assert isinstance(style.styles[1], styles.LabelStyle) + assert style.styles[1].id == "id-a0" + assert style.styles[1].target_id == "target-a0" assert style.styles[1].color == "ff0000ff" assert style.styles[1].color_mode == ColorMode.random assert style.styles[1].scale == 1.0 - assert style.styles[1].heading == 0 - assert style.styles[1].icon.href == "http://example.com/icon.png" - assert isinstance(style.styles[2], styles.LabelStyle) - assert style.styles[2].id == "id-a0" - assert style.styles[2].target_id == "target-a0" + assert isinstance(style.styles[2], styles.LineStyle) + assert style.styles[2].id == "id-l0" + assert style.styles[2].target_id == "target-l0" assert style.styles[2].color == "ff0000ff" - assert style.styles[2].color_mode == ColorMode.random - assert style.styles[2].scale == 1.0 + assert style.styles[2].color_mode == ColorMode.normal + assert style.styles[2].width == 1.0 - assert isinstance(style.styles[3], styles.LineStyle) - assert style.styles[3].id == "id-l0" - assert style.styles[3].target_id == "target-l0" + assert isinstance(style.styles[3], styles.PolyStyle) + assert style.styles[3].id == "id-p0" + assert style.styles[3].target_id == "target-p0" assert style.styles[3].color == "ff0000ff" - assert style.styles[3].color_mode == ColorMode.normal - assert style.styles[3].width == 1.0 - - assert isinstance(style.styles[4], styles.PolyStyle) - assert style.styles[4].id == "id-p0" - assert style.styles[4].target_id == "target-p0" - assert style.styles[4].color == "ff0000ff" - assert style.styles[4].color_mode == ColorMode.random - assert style.styles[4].fill == 0 - assert style.styles[4].outline == 1 + assert style.styles[3].color_mode == ColorMode.random + assert style.styles[3].fill == 0 + assert style.styles[3].outline == 1 + + assert isinstance(style.styles[4], styles.BalloonStyle) + assert style.styles[4].id == "id-b0" + assert style.styles[4].target_id == "target-b0" + assert style.styles[4].bg_color == "7fff0000" + assert style.styles[4].text_color == "ff00ff00" + assert style.styles[4].text == "Hello" + assert style.styles[4].display_mode == DisplayMode.hide def test_stylemap(self) -> None: # noqa: PLR0915 url = styles.StyleUrl(url="#style-0")