reactive-python__reactpy METASEP

docs/source/guides/managing-state/sharing-component-state/_examples/filterable_list/data.json METASEP
[
  {
    "name": "Sushi",
    "description": "Sushi is a traditional Japanese dish of prepared vinegared rice"
  },
  {
    "name": "Dal",
    "description": "The most common way of preparing dal is in the form of a soup to which onions, tomatoes and various spices may be added"
  },
  {
    "name": "Pierogi",
    "description": "Pierogi are filled dumplings made by wrapping unleavened dough around a savoury or sweet filling and cooking in boiling water"
  },
  {
    "name": "Shish Kebab",
    "description": "Shish kebab is a popular meal of skewered and grilled cubes of meat"
  },
  {
    "name": "Dim sum",
    "description": "Dim sum is a large range of small dishes that Cantonese people traditionally enjoy in restaurants for breakfast and lunch"
  }
]

docs/source/guides/adding-interactivity/components-with-state/_examples/when_variables_are_not_enough/data.json METASEP
[
  {
    "name": "Homenaje a la Neurocirugía",
    "artist": "Marta Colvin Andrade",
    "description": "Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg/1024px-Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg",
    "alt": "A bronze statue of two crossed hands delicately holding a human brain in their fingertips."
  },
  {
    "name": "Eternal Presence",
    "artist": "John Woodrow Wilson",
    "description": "Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as \"a symbolic Black presence infused with a sense of universal humanity.\"",
    "url": "https://upload.wikimedia.org/wikipedia/commons/6/6f/Chicago%2C_Illinois_Eternal_Silence1_crop.jpg",
    "alt": "The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity."
  },
  {
    "name": "Moai",
    "artist": "Unknown Artist",
    "description": "Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/5/50/AhuTongariki.JPG",
    "alt": "Three monumental stone busts with the heads that are disproportionately large with somber faces."
  },
  {
    "name": "Blue Nana",
    "artist": "Niki de Saint Phalle",
    "description": "The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Blue_Nana_-_panoramio.jpg/1024px-Blue_Nana_-_panoramio.jpg",
    "alt": "A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy."
  },
  {
    "name": "Cavaliere",
    "artist": "Lamidi Olonade Fakeye",
    "description": "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/3/34/Nigeria%2C_lamidi_olonade_fakeye%2C_cavaliere%2C_1992.jpg",
    "alt": "An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns."
  },
  {
    "name": "Big Bellies",
    "artist": "Alina Szapocznikow",
    "description": "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/92/KMM_Szapocznikow.JPG/200px-KMM_Szapocznikow.JPG",
    "alt": "The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures."
  },
  {
    "name": "Terracotta Army",
    "artist": "Unknown Artist",
    "description": "The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consited of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg/1920px-2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg",
    "alt": "12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor."
  },
  {
    "name": "Lunar Landscape",
    "artist": "Louise Nevelson",
    "description": "Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/72/1999-3-A--J_s.jpg/220px-1999-3-A--J_s.jpg",
    "alt": "A black matte sculpture where the individual elements are initially indistinguishable."
  },
  {
    "name": "Aureole",
    "artist": "Ranjani Shettar",
    "description": "Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a \"fine synthesis of unlikely materials.\"",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/50/Shettar-_5854-sm_%285132866765%29.jpg/399px-Shettar-_5854-sm_%285132866765%29.jpg",
    "alt": "A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light."
  },
  {
    "name": "Hippos",
    "artist": "Taipei Zoo",
    "description": "The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e3/Hippo_sculpture_Taipei_Zoo_20543.jpg/250px-Hippo_sculpture_Taipei_Zoo_20543.jpg",
    "alt": "A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming."
  }
]

docs/source/guides/adding-interactivity/components-with-state/_examples/multiple_state_variables/data.json METASEP
[
  {
    "name": "Homenaje a la Neurocirugía",
    "artist": "Marta Colvin Andrade",
    "description": "Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg/1024px-Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg",
    "alt": "A bronze statue of two crossed hands delicately holding a human brain in their fingertips."
  },
  {
    "name": "Eternal Presence",
    "artist": "John Woodrow Wilson",
    "description": "Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as \"a symbolic Black presence infused with a sense of universal humanity.\"",
    "url": "https://upload.wikimedia.org/wikipedia/commons/6/6f/Chicago%2C_Illinois_Eternal_Silence1_crop.jpg",
    "alt": "The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity."
  },
  {
    "name": "Moai",
    "artist": "Unknown Artist",
    "description": "Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/5/50/AhuTongariki.JPG",
    "alt": "Three monumental stone busts with the heads that are disproportionately large with somber faces."
  },
  {
    "name": "Blue Nana",
    "artist": "Niki de Saint Phalle",
    "description": "The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Blue_Nana_-_panoramio.jpg/1024px-Blue_Nana_-_panoramio.jpg",
    "alt": "A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy."
  },
  {
    "name": "Cavaliere",
    "artist": "Lamidi Olonade Fakeye",
    "description": "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/3/34/Nigeria%2C_lamidi_olonade_fakeye%2C_cavaliere%2C_1992.jpg",
    "alt": "An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns."
  },
  {
    "name": "Big Bellies",
    "artist": "Alina Szapocznikow",
    "description": "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/92/KMM_Szapocznikow.JPG/200px-KMM_Szapocznikow.JPG",
    "alt": "The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures."
  },
  {
    "name": "Terracotta Army",
    "artist": "Unknown Artist",
    "description": "The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consited of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg/1920px-2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg",
    "alt": "12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor."
  },
  {
    "name": "Lunar Landscape",
    "artist": "Louise Nevelson",
    "description": "Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/72/1999-3-A--J_s.jpg/220px-1999-3-A--J_s.jpg",
    "alt": "A black matte sculpture where the individual elements are initially indistinguishable."
  },
  {
    "name": "Aureole",
    "artist": "Ranjani Shettar",
    "description": "Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a \"fine synthesis of unlikely materials.\"",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/50/Shettar-_5854-sm_%285132866765%29.jpg/399px-Shettar-_5854-sm_%285132866765%29.jpg",
    "alt": "A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light."
  },
  {
    "name": "Hippos",
    "artist": "Taipei Zoo",
    "description": "The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e3/Hippo_sculpture_Taipei_Zoo_20543.jpg/250px-Hippo_sculpture_Taipei_Zoo_20543.jpg",
    "alt": "A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming."
  }
]

docs/source/guides/adding-interactivity/components-with-state/_examples/isolated_state/data.json METASEP
[
  {
    "name": "Homenaje a la Neurocirugía",
    "artist": "Marta Colvin Andrade",
    "description": "Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg/1024px-Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg",
    "alt": "A bronze statue of two crossed hands delicately holding a human brain in their fingertips."
  },
  {
    "name": "Eternal Presence",
    "artist": "John Woodrow Wilson",
    "description": "Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as \"a symbolic Black presence infused with a sense of universal humanity.\"",
    "url": "https://upload.wikimedia.org/wikipedia/commons/6/6f/Chicago%2C_Illinois_Eternal_Silence1_crop.jpg",
    "alt": "The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity."
  },
  {
    "name": "Moai",
    "artist": "Unknown Artist",
    "description": "Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/5/50/AhuTongariki.JPG",
    "alt": "Three monumental stone busts with the heads that are disproportionately large with somber faces."
  },
  {
    "name": "Blue Nana",
    "artist": "Niki de Saint Phalle",
    "description": "The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Blue_Nana_-_panoramio.jpg/1024px-Blue_Nana_-_panoramio.jpg",
    "alt": "A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy."
  },
  {
    "name": "Cavaliere",
    "artist": "Lamidi Olonade Fakeye",
    "description": "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/3/34/Nigeria%2C_lamidi_olonade_fakeye%2C_cavaliere%2C_1992.jpg",
    "alt": "An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns."
  },
  {
    "name": "Big Bellies",
    "artist": "Alina Szapocznikow",
    "description": "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/92/KMM_Szapocznikow.JPG/200px-KMM_Szapocznikow.JPG",
    "alt": "The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures."
  },
  {
    "name": "Terracotta Army",
    "artist": "Unknown Artist",
    "description": "The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consited of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg/1920px-2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg",
    "alt": "12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor."
  },
  {
    "name": "Lunar Landscape",
    "artist": "Louise Nevelson",
    "description": "Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/72/1999-3-A--J_s.jpg/220px-1999-3-A--J_s.jpg",
    "alt": "A black matte sculpture where the individual elements are initially indistinguishable."
  },
  {
    "name": "Aureole",
    "artist": "Ranjani Shettar",
    "description": "Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a \"fine synthesis of unlikely materials.\"",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/50/Shettar-_5854-sm_%285132866765%29.jpg/399px-Shettar-_5854-sm_%285132866765%29.jpg",
    "alt": "A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light."
  },
  {
    "name": "Hippos",
    "artist": "Taipei Zoo",
    "description": "The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e3/Hippo_sculpture_Taipei_Zoo_20543.jpg/250px-Hippo_sculpture_Taipei_Zoo_20543.jpg",
    "alt": "A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming."
  }
]

docs/source/guides/adding-interactivity/components-with-state/_examples/adding_state_variable/data.json METASEP
[
  {
    "name": "Homenaje a la Neurocirugía",
    "artist": "Marta Colvin Andrade",
    "description": "Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg/1024px-Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg",
    "alt": "A bronze statue of two crossed hands delicately holding a human brain in their fingertips."
  },
  {
    "name": "Eternal Presence",
    "artist": "John Woodrow Wilson",
    "description": "Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as \"a symbolic Black presence infused with a sense of universal humanity.\"",
    "url": "https://upload.wikimedia.org/wikipedia/commons/6/6f/Chicago%2C_Illinois_Eternal_Silence1_crop.jpg",
    "alt": "The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity."
  },
  {
    "name": "Moai",
    "artist": "Unknown Artist",
    "description": "Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/5/50/AhuTongariki.JPG",
    "alt": "Three monumental stone busts with the heads that are disproportionately large with somber faces."
  },
  {
    "name": "Blue Nana",
    "artist": "Niki de Saint Phalle",
    "description": "The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Blue_Nana_-_panoramio.jpg/1024px-Blue_Nana_-_panoramio.jpg",
    "alt": "A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy."
  },
  {
    "name": "Cavaliere",
    "artist": "Lamidi Olonade Fakeye",
    "description": "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/3/34/Nigeria%2C_lamidi_olonade_fakeye%2C_cavaliere%2C_1992.jpg",
    "alt": "An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns."
  },
  {
    "name": "Big Bellies",
    "artist": "Alina Szapocznikow",
    "description": "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/92/KMM_Szapocznikow.JPG/200px-KMM_Szapocznikow.JPG",
    "alt": "The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures."
  },
  {
    "name": "Terracotta Army",
    "artist": "Unknown Artist",
    "description": "The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consited of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg/1920px-2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg",
    "alt": "12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor."
  },
  {
    "name": "Lunar Landscape",
    "artist": "Louise Nevelson",
    "description": "Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/72/1999-3-A--J_s.jpg/220px-1999-3-A--J_s.jpg",
    "alt": "A black matte sculpture where the individual elements are initially indistinguishable."
  },
  {
    "name": "Aureole",
    "artist": "Ranjani Shettar",
    "description": "Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a \"fine synthesis of unlikely materials.\"",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/50/Shettar-_5854-sm_%285132866765%29.jpg/399px-Shettar-_5854-sm_%285132866765%29.jpg",
    "alt": "A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light."
  },
  {
    "name": "Hippos",
    "artist": "Taipei Zoo",
    "description": "The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.",
    "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e3/Hippo_sculpture_Taipei_Zoo_20543.jpg/250px-Hippo_sculpture_Taipei_Zoo_20543.jpg",
    "alt": "A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming."
  }
]

docs/source/guides/managing-state/sharing-component-state/_examples/synced_inputs/main.py METASEP
from reactpy import component, hooks, html, run


@component
def SyncedInputs():
    value, set_value = hooks.use_state("")
    return html.p(
        Input("First input", value, set_value),
        Input("Second input", value, set_value),
    )


@component
def Input(label, value, set_value):
    def handle_change(event):
        set_value(event["target"]["value"])

    return html.label(
        label + " ", html.input({"value": value, "on_change": handle_change})
    )


run(SyncedInputs)

docs/source/guides/managing-state/sharing-component-state/_examples/filterable_list/main.py METASEP
import json
from pathlib import Path

from reactpy import component, hooks, html, run


HERE = Path(__file__)
DATA_PATH = HERE.parent / "data.json"
food_data = json.loads(DATA_PATH.read_text())


@component
def FilterableList():
    value, set_value = hooks.use_state("")
    return html.p(Search(value, set_value), html.hr(), Table(value, set_value))


@component
def Search(value, set_value):
    def handle_change(event):
        set_value(event["target"]["value"])

    return html.label(
        "Search by Food Name: ",
        html.input({"value": value, "on_change": handle_change}),
    )


@component
def Table(value, set_value):
    rows = []
    for row in food_data:
        name = html.td(row["name"])
        descr = html.td(row["description"])
        tr = html.tr(name, descr, value)
        if value == "":
            rows.append(tr)
        else:
            if value.lower() in row["name"].lower():
                rows.append(tr)
        headers = html.tr(html.td(html.b("name")), html.td(html.b("description")))
    table = html.table(html.thead(headers), html.tbody(rows))
    return table


run(FilterableList)

docs/source/guides/adding-interactivity/components-with-state/_examples/when_variables_are_not_enough/main.py METASEP
# flake8: noqa
# errors F841,F823 for `index = index + 1` inside the closure

# :lines: 7-
# :linenos:

import json
from pathlib import Path

from reactpy import component, html, run


HERE = Path(__file__)
DATA_PATH = HERE.parent / "data.json"
sculpture_data = json.loads(DATA_PATH.read_text())


@component
def Gallery():
    index = 0

    def handle_click(event):
        index = index + 1

    bounded_index = index % len(sculpture_data)
    sculpture = sculpture_data[bounded_index]
    alt = sculpture["alt"]
    artist = sculpture["artist"]
    description = sculpture["description"]
    name = sculpture["name"]
    url = sculpture["url"]

    return html.div(
        html.button({"on_click": handle_click}, "Next"),
        html.h2(name, " by ", artist),
        html.p(f"({bounded_index + 1} or {len(sculpture_data)})"),
        html.img({"src": url, "alt": alt, "style": {"height": "200px"}}),
        html.p(description),
    )


run(Gallery)

docs/source/guides/adding-interactivity/components-with-state/_examples/multiple_state_variables/main.py METASEP
import json
from pathlib import Path

from reactpy import component, hooks, html, run


HERE = Path(__file__)
DATA_PATH = HERE.parent / "data.json"
sculpture_data = json.loads(DATA_PATH.read_text())


@component
def Gallery():
    index, set_index = hooks.use_state(0)
    show_more, set_show_more = hooks.use_state(False)

    def handle_next_click(event):
        set_index(index + 1)

    def handle_more_click(event):
        set_show_more(not show_more)

    bounded_index = index % len(sculpture_data)
    sculpture = sculpture_data[bounded_index]
    alt = sculpture["alt"]
    artist = sculpture["artist"]
    description = sculpture["description"]
    name = sculpture["name"]
    url = sculpture["url"]

    return html.div(
        html.button({"on_click": handle_next_click}, "Next"),
        html.h2(name, " by ", artist),
        html.p(f"({bounded_index + 1} or {len(sculpture_data)})"),
        html.img({"src": url, "alt": alt, "style": {"height": "200px"}}),
        html.div(
            html.button(
                {"on_click": handle_more_click},
                f"{('Show' if show_more else 'Hide')} details",
            ),
            (html.p(description) if show_more else ""),
        ),
    )


run(Gallery)

docs/source/guides/adding-interactivity/components-with-state/_examples/isolated_state/main.py METASEP
import json
from pathlib import Path

from reactpy import component, hooks, html, run


HERE = Path(__file__)
DATA_PATH = HERE.parent / "data.json"
sculpture_data = json.loads(DATA_PATH.read_text())


@component
def Gallery():
    index, set_index = hooks.use_state(0)
    show_more, set_show_more = hooks.use_state(False)

    def handle_next_click(event):
        set_index(index + 1)

    def handle_more_click(event):
        set_show_more(not show_more)

    bounded_index = index % len(sculpture_data)
    sculpture = sculpture_data[bounded_index]
    alt = sculpture["alt"]
    artist = sculpture["artist"]
    description = sculpture["description"]
    name = sculpture["name"]
    url = sculpture["url"]

    return html.div(
        html.button({"on_click": handle_next_click}, "Next"),
        html.h2(name, " by ", artist),
        html.p(f"({bounded_index + 1} or {len(sculpture_data)})"),
        html.img({"src": url, "alt": alt, "style": {"height": "200px"}}),
        html.div(
            html.button(
                {"on_click": handle_more_click},
                f"{('Show' if show_more else 'Hide')} details",
            ),
            (html.p(description) if show_more else ""),
        ),
    )


@component
def App():
    return html.div(
        html.section({"style": {"width": "50%", "float": "left"}}, Gallery()),
        html.section({"style": {"width": "50%", "float": "left"}}, Gallery()),
    )


run(App)

docs/source/guides/adding-interactivity/components-with-state/_examples/adding_state_variable/main.py METASEP
import json
from pathlib import Path

from reactpy import component, hooks, html, run


HERE = Path(__file__)
DATA_PATH = HERE.parent / "data.json"
sculpture_data = json.loads(DATA_PATH.read_text())


@component
def Gallery():
    index, set_index = hooks.use_state(0)

    def handle_click(event):
        set_index(index + 1)

    bounded_index = index % len(sculpture_data)
    sculpture = sculpture_data[bounded_index]
    alt = sculpture["alt"]
    artist = sculpture["artist"]
    description = sculpture["description"]
    name = sculpture["name"]
    url = sculpture["url"]

    return html.div(
        html.button({"on_click": handle_click}, "Next"),
        html.h2(name, " by ", artist),
        html.p(f"({bounded_index + 1} of {len(sculpture_data)})"),
        html.img({"src": url, "alt": alt, "style": {"height": "200px"}}),
        html.p(description),
    )


run(Gallery)

docs/source/reference/_examples/character_movement/static/bunny.png METASEP

docs/source/guides/getting-started/_static/embed-reactpy-view/screenshot.png METASEP

docs/source/guides/getting-started/_static/embed-reactpy-view/index.html METASEP
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Example App</title>
  </head>
  <body>
    <h1>This is an Example App</h1>
    <p>Just below is an embedded ReactPy view...</p>
    <div id="reactpy-app" />
    <script type="module">
      import {
        mountWithLayoutServer,
        LayoutServerInfo,
      } from "https://esm.sh/@reactpy/client@0.38.0";

      const serverInfo = new LayoutServerInfo({
        host: document.location.hostname,
        port: document.location.port,
        path: "_reactpy",
        query: queryParams.user.toString(),
        secure: document.location.protocol == "https:",
      });

      mountLayoutWithWebSocket(
        document.getElementById("reactpy-app"),
        serverInfo
      );
    </script>
  </body>
</html>

docs/source/guides/escape-hatches/_examples/super_simple_chart/super-simple-chart.js METASEP
import { h, render } from "https://unpkg.com/preact?module";
import htm from "https://unpkg.com/htm?module";

const html = htm.bind(h);

export function bind(node, config) {
  return {
    create: (component, props, children) => h(component, props, ...children),
    render: (element) => render(element, node),
    unmount: () => render(null, node),
  };
}

export function SuperSimpleChart(props) {
  const data = props.data;
  const lastDataIndex = data.length - 1;

  const options = {
    height: props.height || 100,
    width: props.width || 100,
    color: props.color || "blue",
    lineWidth: props.lineWidth || 2,
    axisColor: props.axisColor || "black",
  };

  const xData = data.map((point) => point.x);
  const yData = data.map((point) => point.y);

  const domain = {
    xMin: Math.min(...xData),
    xMax: Math.max(...xData),
    yMin: Math.min(...yData),
    yMax: Math.max(...yData),
  };

  return html`<svg
    width="${options.width}px"
    height="${options.height}px"
    viewBox="0 0 ${options.width} ${options.height}"
  >
    ${makePath(props, domain, data, options)} ${makeAxis(props, options)}
  </svg>`;
}

function makePath(props, domain, data, options) {
  const { xMin, xMax, yMin, yMax } = domain;
  const { width, height } = options;
  const getSvgX = (x) => ((x - xMin) / (xMax - xMin)) * width;
  const getSvgY = (y) => height - ((y - yMin) / (yMax - yMin)) * height;

  let pathD =
    `M ${getSvgX(data[0].x)} ${getSvgY(data[0].y)} ` +
    data.map(({ x, y }, i) => `L ${getSvgX(x)} ${getSvgY(y)}`).join(" ");

  return html`<path
    d="${pathD}"
    style=${{
      stroke: options.color,
      strokeWidth: options.lineWidth,
      fill: "none",
    }}
  />`;
}

function makeAxis(props, options) {
  return html`<g>
    <line
      x1="0"
      y1=${options.height}
      x2=${options.width}
      y2=${options.height}
      style=${{ stroke: options.axisColor, strokeWidth: options.lineWidth * 2 }}
    />
    <line
      x1="0"
      y1="0"
      x2="0"
      y2=${options.height}
      style=${{ stroke: options.axisColor, strokeWidth: options.lineWidth * 2 }}
    />
  </g>`;
}

docs/source/guides/adding-interactivity/state-as-a-snapshot/_static/reactpy-state-change.png METASEP

docs/source/guides/adding-interactivity/state-as-a-snapshot/_static/direct-state-change.png METASEP

docs/source/guides/getting-started/_static/embed-reactpy-view/main.py METASEP
from sanic import Sanic
from sanic.response import file

from reactpy import component, html
from reactpy.backend.sanic import Options, configure


app = Sanic("MyApp")


@app.route("/")
async def index(request):
    return await file("index.html")


@component
def ReactPyView():
    return html.code("This text came from an ReactPy App")


configure(app, ReactPyView, Options(url_prefix="/_reactpy"))

app.run(host="127.0.0.1", port=5000)

docs/source/guides/escape-hatches/_examples/super_simple_chart/main.py METASEP
from pathlib import Path

from reactpy import component, run, web


file = Path(__file__).parent / "super-simple-chart.js"
ssc = web.module_from_file("super-simple-chart", file, fallback="⌛")
SuperSimpleChart = web.export(ssc, "SuperSimpleChart")


@component
def App():
    return SuperSimpleChart(
        {
            "data": [
                {"x": 1, "y": 2},
                {"x": 2, "y": 4},
                {"x": 3, "y": 7},
                {"x": 4, "y": 3},
                {"x": 5, "y": 5},
                {"x": 6, "y": 9},
                {"x": 7, "y": 6},
            ],
            "height": 300,
            "width": 500,
            "color": "royalblue",
            "lineWidth": 4,
            "axisColor": "silver",
        }
    )


run(App)

docs/source/guides/creating-interfaces/your-first-components/_examples/wrap_in_fragment.py METASEP
from reactpy import component, html, run


@component
def MyTodoList():
    return html._(
        html.h1("My Todo List"),
        html.img({"src": "https://picsum.photos/id/0/500/200"}),
        html.ul(html.li("The first thing I need to do is...")),
    )


run(MyTodoList)

docs/source/guides/creating-interfaces/your-first-components/_examples/wrap_in_div.py METASEP
from reactpy import component, html, run


@component
def MyTodoList():
    return html.div(
        html.h1("My Todo List"),
        html.img({"src": "https://picsum.photos/id/0/500/300"}),
        html.ul(html.li("The first thing I need to do is...")),
    )


run(MyTodoList)

docs/source/guides/creating-interfaces/your-first-components/_examples/todo_list.py METASEP
from reactpy import component, html, run


@component
def Item(name, done):
    return html.li(name)


@component
def TodoList():
    return html.section(
        html.h1("My Todo List"),
        html.ul(
            Item("Find a cool problem to solve", done=True),
            Item("Build an app to solve it", done=True),
            Item("Share that app with the world!", done=False),
        ),
    )


run(TodoList)

docs/source/guides/creating-interfaces/your-first-components/_examples/simple_photo.py METASEP
from reactpy import component, html, run


@component
def Photo():
    return html.img(
        {
            "src": "https://picsum.photos/id/237/500/300",
            "style": {"width": "50%"},
            "alt": "Puppy",
        }
    )


run(Photo)

docs/source/guides/creating-interfaces/your-first-components/_examples/parametrized_photos.py METASEP
from reactpy import component, html, run


@component
def Photo(alt_text, image_id):
    return html.img(
        {
            "src": f"https://picsum.photos/id/{image_id}/500/200",
            "style": {"width": "50%"},
            "alt": alt_text,
        }
    )


@component
def Gallery():
    return html.section(
        html.h1("Photo Gallery"),
        Photo("Landscape", image_id=830),
        Photo("City", image_id=274),
        Photo("Puppy", image_id=237),
    )


run(Gallery)

docs/source/guides/creating-interfaces/your-first-components/_examples/nested_photos.py METASEP
from reactpy import component, html, run


@component
def Photo():
    return html.img(
        {
            "src": "https://picsum.photos/id/274/500/300",
            "style": {"width": "30%"},
            "alt": "Ray Charles",
        }
    )


@component
def Gallery():
    return html.section(
        html.h1("Famous Musicians"),
        Photo(),
        Photo(),
        Photo(),
    )


run(Gallery)

docs/source/guides/creating-interfaces/your-first-components/_examples/good_conditional_todo_list.py METASEP
from reactpy import component, html, run


@component
def Item(name, done):
    return html.li(name, " ✔" if done else "")


@component
def TodoList():
    return html.section(
        html.h1("My Todo List"),
        html.ul(
            Item("Find a cool problem to solve", done=True),
            Item("Build an app to solve it", done=True),
            Item("Share that app with the world!", done=False),
        ),
    )


run(TodoList)

docs/source/guides/creating-interfaces/your-first-components/_examples/bad_conditional_todo_list.py METASEP
from reactpy import component, html, run


@component
def Item(name, done):
    if done:
        return html.li(name, " ✔")
    else:
        return html.li(name)


@component
def TodoList():
    return html.section(
        html.h1("My Todo List"),
        html.ul(
            Item("Find a cool problem to solve", done=True),
            Item("Build an app to solve it", done=True),
            Item("Share that app with the world!", done=False),
        ),
    )


run(TodoList)

docs/source/guides/creating-interfaces/rendering-data/_examples/todo_list_with_keys.py METASEP
from reactpy import component, html, run


@component
def DataList(items, filter_by_priority=None, sort_by_priority=False):
    if filter_by_priority is not None:
        items = [i for i in items if i["priority"] <= filter_by_priority]
    if sort_by_priority:
        items = list(sorted(items, key=lambda i: i["priority"]))
    list_item_elements = [html.li({"key": i["id"]}, i["text"]) for i in items]
    return html.ul(list_item_elements)


@component
def TodoList():
    tasks = [
        {"id": 0, "text": "Make breakfast", "priority": 0},
        {"id": 1, "text": "Feed the dog", "priority": 0},
        {"id": 2, "text": "Do laundry", "priority": 2},
        {"id": 3, "text": "Go on a run", "priority": 1},
        {"id": 4, "text": "Clean the house", "priority": 2},
        {"id": 5, "text": "Go to the grocery store", "priority": 2},
        {"id": 6, "text": "Do some coding", "priority": 1},
        {"id": 7, "text": "Read a book", "priority": 1},
    ]
    return html.section(
        html.h1("My Todo List"),
        DataList(tasks, filter_by_priority=1, sort_by_priority=True),
    )


run(TodoList)

docs/source/guides/creating-interfaces/rendering-data/_examples/todo_from_list.py METASEP
from reactpy import component, html, run


@component
def DataList(items):
    list_item_elements = [html.li(text) for text in items]
    return html.ul(list_item_elements)


@component
def TodoList():
    tasks = [
        "Make breakfast (important)",
        "Feed the dog (important)",
        "Do laundry",
        "Go on a run (important)",
        "Clean the house",
        "Go to the grocery store",
        "Do some coding",
        "Read a book (important)",
    ]
    return html.section(
        html.h1("My Todo List"),
        DataList(tasks),
    )


run(TodoList)

docs/source/guides/creating-interfaces/rendering-data/_examples/sorted_and_filtered_todo_list.py METASEP
from reactpy import component, html, run


@component
def DataList(items, filter_by_priority=None, sort_by_priority=False):
    if filter_by_priority is not None:
        items = [i for i in items if i["priority"] <= filter_by_priority]
    if sort_by_priority:
        items = list(sorted(items, key=lambda i: i["priority"]))
    list_item_elements = [html.li(i["text"]) for i in items]
    return html.ul(list_item_elements)


@component
def TodoList():
    tasks = [
        {"text": "Make breakfast", "priority": 0},
        {"text": "Feed the dog", "priority": 0},
        {"text": "Do laundry", "priority": 2},
        {"text": "Go on a run", "priority": 1},
        {"text": "Clean the house", "priority": 2},
        {"text": "Go to the grocery store", "priority": 2},
        {"text": "Do some coding", "priority": 1},
        {"text": "Read a book", "priority": 1},
    ]
    return html.section(
        html.h1("My Todo List"),
        DataList(tasks, filter_by_priority=1, sort_by_priority=True),
    )


run(TodoList)

docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/set_counter_3_times.py METASEP
from reactpy import component, html, run, use_state


@component
def Counter():
    number, set_number = use_state(0)

    def handle_click(event):
        set_number(number + 1)
        set_number(number + 1)
        set_number(number + 1)

    return html.div(
        html.h1(number),
        html.button({"on_click": handle_click}, "Increment"),
    )


run(Counter)

docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/send_message.py METASEP
from reactpy import component, event, html, run, use_state


@component
def App():
    is_sent, set_is_sent = use_state(False)
    message, set_message = use_state("")

    if is_sent:
        return html.div(
            html.h1("Message sent!"),
            html.button(
                {"on_click": lambda event: set_is_sent(False)}, "Send new message?"
            ),
        )

    @event(prevent_default=True)
    def handle_submit(event):
        set_message("")
        set_is_sent(True)

    return html.form(
        {"on_submit": handle_submit, "style": {"display": "inline-grid"}},
        html.textarea(
            {
                "placeholder": "Your message here...",
                "value": message,
                "on_change": lambda event: set_message(event["target"]["value"]),
            }
        ),
        html.button({"type": "submit"}, "Send"),
    )


run(App)

docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/print_count_after_set.py METASEP
from reactpy import component, html, run, use_state


@component
def Counter():
    number, set_number = use_state(0)

    def handle_click(event):
        set_number(number + 5)
        print(number)

    return html.div(
        html.h1(number),
        html.button({"on_click": handle_click}, "Increment"),
    )


run(Counter)

docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/print_chat_message.py METASEP
import asyncio

from reactpy import component, event, html, run, use_state


@component
def App():
    recipient, set_recipient = use_state("Alice")
    message, set_message = use_state("")

    @event(prevent_default=True)
    async def handle_submit(event):
        set_message("")
        print("About to send message...")
        await asyncio.sleep(5)
        print(f"Sent '{message}' to {recipient}")

    return html.form(
        {"on_submit": handle_submit, "style": {"display": "inline-grid"}},
        html.label(
            {},
            "To: ",
            html.select(
                {
                    "value": recipient,
                    "on_change": lambda event: set_recipient(event["target"]["value"]),
                },
                html.option({"value": "Alice"}, "Alice"),
                html.option({"value": "Bob"}, "Bob"),
            ),
        ),
        html.input(
            {
                "type": "text",
                "placeholder": "Your message...",
                "value": message,
                "on_change": lambda event: set_message(event["target"]["value"]),
            }
        ),
        html.button({"type": "submit"}, "Send"),
    )


run(App)

docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/delayed_print_after_set.py METASEP
import asyncio

from reactpy import component, html, run, use_state


@component
def Counter():
    number, set_number = use_state(0)

    async def handle_click(event):
        set_number(number + 5)
        print("about to print...")
        await asyncio.sleep(3)
        print(number)

    return html.div(
        html.h1(number),
        html.button({"on_click": handle_click}, "Increment"),
    )


run(Counter)

docs/source/guides/adding-interactivity/responding-to-events/_examples/stop_event_propagation.py METASEP
from reactpy import component, event, hooks, html, run


@component
def DivInDiv():
    stop_propagatation, set_stop_propagatation = hooks.use_state(True)
    inner_count, set_inner_count = hooks.use_state(0)
    outer_count, set_outer_count = hooks.use_state(0)

    div_in_div = html.div(
        {
            "on_click": lambda event: set_outer_count(outer_count + 1),
            "style": {"height": "100px", "width": "100px", "background_color": "red"},
        },
        html.div(
            {
                "on_click": event(
                    lambda event: set_inner_count(inner_count + 1),
                    stop_propagation=stop_propagatation,
                ),
                "style": {
                    "height": "50px",
                    "width": "50px",
                    "background_color": "blue",
                },
            }
        ),
    )

    return html.div(
        html.button(
            {"on_click": lambda event: set_stop_propagatation(not stop_propagatation)},
            "Toggle Propogation",
        ),
        html.pre(f"Will propagate: {not stop_propagatation}"),
        html.pre(f"Inner click count: {inner_count}"),
        html.pre(f"Outer click count: {outer_count}"),
        div_in_div,
    )


run(DivInDiv)

docs/source/guides/adding-interactivity/responding-to-events/_examples/prevent_default_event_actions.py METASEP
from reactpy import component, event, html, run


@component
def DoNotChangePages():
    return html.div(
        html.p("Normally clicking this link would take you to a new page"),
        html.a(
            {
                "on_click": event(lambda event: None, prevent_default=True),
                "href": "https://google.com",
            },
            "https://google.com",
        ),
    )


run(DoNotChangePages)

docs/source/guides/adding-interactivity/responding-to-events/_examples/button_prints_message.py METASEP
from reactpy import component, html, run


@component
def PrintButton(display_text, message_text):
    def handle_event(event):
        print(message_text)

    return html.button({"on_click": handle_event}, display_text)


@component
def App():
    return html.div(
        PrintButton("Play", "Playing"),
        PrintButton("Pause", "Paused"),
    )


run(App)

docs/source/guides/adding-interactivity/responding-to-events/_examples/button_prints_event.py METASEP
from reactpy import component, html, run


@component
def Button():
    def handle_event(event):
        print(event)

    return html.button({"on_click": handle_event}, "Click me!")


run(Button)

docs/source/guides/adding-interactivity/responding-to-events/_examples/button_handler_as_arg.py METASEP
from reactpy import component, html, run


@component
def Button(display_text, on_click):
    return html.button({"on_click": on_click}, display_text)


@component
def PlayButton(movie_name):
    def handle_click(event):
        print(f"Playing {movie_name}")

    return Button(f"Play {movie_name}", on_click=handle_click)


@component
def FastForwardButton():
    def handle_click(event):
        print("Skipping ahead")

    return Button("Fast forward", on_click=handle_click)


@component
def App():
    return html.div(
        PlayButton("Buena Vista Social Club"),
        FastForwardButton(),
    )


run(App)

docs/source/guides/adding-interactivity/responding-to-events/_examples/button_does_nothing.py METASEP
from reactpy import component, html, run


@component
def Button():
    return html.button("I don't do anything yet")


run(Button)

docs/source/guides/adding-interactivity/responding-to-events/_examples/button_async_handlers.py METASEP
import asyncio

from reactpy import component, html, run


@component
def ButtonWithDelay(message, delay):
    async def handle_event(event):
        await asyncio.sleep(delay)
        print(message)

    return html.button({"on_click": handle_event}, message)


@component
def App():
    return html.div(
        ButtonWithDelay("print 3 seconds later", delay=3),
        ButtonWithDelay("print immediately", delay=0),
    )


run(App)

docs/source/guides/adding-interactivity/responding-to-events/_examples/audio_player.py METASEP
import json

import reactpy


@reactpy.component
def PlayDinosaurSound():
    event, set_event = reactpy.hooks.use_state(None)
    return reactpy.html.div(
        reactpy.html.audio(
            {
                "controls": True,
                "on_time_update": lambda e: set_event(e),
                "src": "https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3",
            }
        ),
        reactpy.html.pre(json.dumps(event, indent=2)),
    )


reactpy.run(PlayDinosaurSound)

docs/source/guides/adding-interactivity/multiple-state-updates/_examples/set_state_function.py METASEP
from reactpy import component, html, run, use_state


def increment(old_number):
    new_number = old_number + 1
    return new_number


@component
def Counter():
    number, set_number = use_state(0)

    def handle_click(event):
        set_number(increment)
        set_number(increment)
        set_number(increment)

    return html.div(
        html.h1(number),
        html.button({"on_click": handle_click}, "Increment"),
    )


run(Counter)

docs/source/guides/adding-interactivity/multiple-state-updates/_examples/set_color_3_times.py METASEP
from reactpy import component, html, run, use_state


@component
def ColorButton():
    color, set_color = use_state("gray")

    def handle_click(event):
        set_color("orange")
        set_color("pink")
        set_color("blue")

    def handle_reset(event):
        set_color("gray")

    return html.div(
        html.button(
            {"on_click": handle_click, "style": {"background_color": color}},
            "Set Color",
        ),
        html.button(
            {"on_click": handle_reset, "style": {"background_color": color}}, "Reset"
        ),
    )


run(ColorButton)

docs/source/guides/adding-interactivity/multiple-state-updates/_examples/delay_before_set_count.py METASEP
import asyncio

from reactpy import component, html, run, use_state


@component
def Counter():
    number, set_number = use_state(0)

    async def handle_click(event):
        await asyncio.sleep(3)
        set_number(number + 1)

    return html.div(
        html.h1(number),
        html.button({"on_click": handle_click}, "Increment"),
    )


run(Counter)

docs/source/guides/adding-interactivity/multiple-state-updates/_examples/delay_before_count_updater.py METASEP
import asyncio

from reactpy import component, html, run, use_state


@component
def Counter():
    number, set_number = use_state(0)

    async def handle_click(event):
        await asyncio.sleep(3)
        set_number(lambda old_number: old_number + 1)

    return html.div(
        html.h1(number),
        html.button({"on_click": handle_click}, "Increment"),
    )


run(Counter)

docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/set_update.py METASEP
from reactpy import component, html, run, use_state


@component
def Grid():
    line_size = 5
    selected_indices, set_selected_indices = use_state(set())

    def make_handle_click(index):
        def handle_click(event):
            set_selected_indices(selected_indices | {index})

        return handle_click

    return html.div(
        {"style": {"display": "flex", "flex-direction": "row"}},
        [
            html.div(
                {
                    "on_click": make_handle_click(index),
                    "style": {
                        "height": "30px",
                        "width": "30px",
                        "background_color": "black"
                        if index in selected_indices
                        else "white",
                        "outline": "1px solid grey",
                        "cursor": "pointer",
                    },
                    "key": index,
                }
            )
            for index in range(line_size)
        ],
    )


run(Grid)

docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/set_remove.py METASEP
from reactpy import component, html, run, use_state


@component
def Grid():
    line_size = 5
    selected_indices, set_selected_indices = use_state({1, 2, 4})

    def make_handle_click(index):
        def handle_click(event):
            if index in selected_indices:
                set_selected_indices(selected_indices - {index})
            else:
                set_selected_indices(selected_indices | {index})

        return handle_click

    return html.div(
        {"style": {"display": "flex", "flex-direction": "row"}},
        [
            html.div(
                {
                    "on_click": make_handle_click(index),
                    "style": {
                        "height": "30px",
                        "width": "30px",
                        "background_color": "black"
                        if index in selected_indices
                        else "white",
                        "outline": "1px solid grey",
                        "cursor": "pointer",
                    },
                    "key": index,
                }
            )
            for index in range(line_size)
        ],
    )


run(Grid)

docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/moving_dot_broken.py METASEP
# :linenos:

from reactpy import component, html, run, use_state


@component
def MovingDot():
    position, _ = use_state({"x": 0, "y": 0})

    def handle_pointer_move(event):
        outer_div_info = event["currentTarget"]
        outer_div_bounds = outer_div_info["boundingClientRect"]
        position["x"] = event["clientX"] - outer_div_bounds["x"]
        position["y"] = event["clientY"] - outer_div_bounds["y"]

    return html.div(
        {
            "on_pointer_move": handle_pointer_move,
            "style": {
                "position": "relative",
                "height": "200px",
                "width": "100%",
                "background_color": "white",
            },
        },
        html.div(
            {
                "style": {
                    "position": "absolute",
                    "background_color": "red",
                    "border_radius": "50%",
                    "width": "20px",
                    "height": "20px",
                    "left": "-10px",
                    "top": "-10px",
                    "transform": f"translate({position['x']}px, {position['y']}px)",
                }
            }
        ),
    )


run(MovingDot)

docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/moving_dot.py METASEP
from reactpy import component, html, run, use_state


@component
def MovingDot():
    position, set_position = use_state({"x": 0, "y": 0})

    async def handle_pointer_move(event):
        outer_div_info = event["currentTarget"]
        outer_div_bounds = outer_div_info["boundingClientRect"]
        set_position(
            {
                "x": event["clientX"] - outer_div_bounds["x"],
                "y": event["clientY"] - outer_div_bounds["y"],
            }
        )

    return html.div(
        {
            "on_pointer_move": handle_pointer_move,
            "style": {
                "position": "relative",
                "height": "200px",
                "width": "100%",
                "background_color": "white",
            },
        },
        html.div(
            {
                "style": {
                    "position": "absolute",
                    "background_color": "red",
                    "border_radius": "50%",
                    "width": "20px",
                    "height": "20px",
                    "left": "-10px",
                    "top": "-10px",
                    "transform": f"translate({position['x']}px, {position['y']}px)",
                }
            }
        ),
    )


run(MovingDot)

docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_replace.py METASEP
from reactpy import component, html, run, use_state


@component
def CounterList():
    counters, set_counters = use_state([0, 0, 0])

    def make_increment_click_handler(index):
        def handle_click(event):
            new_value = counters[index] + 1
            set_counters(counters[:index] + [new_value] + counters[index + 1 :])

        return handle_click

    return html.ul(
        [
            html.li(
                {"key": index},
                count,
                html.button({"on_click": make_increment_click_handler(index)}, "+1"),
            )
            for index, count in enumerate(counters)
        ]
    )


run(CounterList)

docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_remove.py METASEP
from reactpy import component, html, run, use_state


@component
def ArtistList():
    artist_to_add, set_artist_to_add = use_state("")
    artists, set_artists = use_state(
        ["Marta Colvin Andrade", "Lamidi Olonade Fakeye", "Louise Nevelson"]
    )

    def handle_change(event):
        set_artist_to_add(event["target"]["value"])

    def handle_add_click(event):
        if artist_to_add not in artists:
            set_artists([*artists, artist_to_add])
            set_artist_to_add("")

    def make_handle_delete_click(index):
        def handle_click(event):
            set_artists(artists[:index] + artists[index + 1 :])

        return handle_click

    return html.div(
        html.h1("Inspiring sculptors:"),
        html.input({"value": artist_to_add, "on_change": handle_change}),
        html.button({"on_click": handle_add_click}, "add"),
        html.ul(
            [
                html.li(
                    {"key": name},
                    name,
                    html.button(
                        {"on_click": make_handle_delete_click(index)}, "delete"
                    ),
                )
                for index, name in enumerate(artists)
            ]
        ),
    )


run(ArtistList)

docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_re_order.py METASEP
from reactpy import component, html, run, use_state


@component
def ArtistList():
    artists, set_artists = use_state(
        ["Marta Colvin Andrade", "Lamidi Olonade Fakeye", "Louise Nevelson"]
    )

    def handle_sort_click(event):
        set_artists(list(sorted(artists)))

    def handle_reverse_click(event):
        set_artists(list(reversed(artists)))

    return html.div(
        html.h1("Inspiring sculptors:"),
        html.button({"on_click": handle_sort_click}, "sort"),
        html.button({"on_click": handle_reverse_click}, "reverse"),
        html.ul([html.li({"key": name}, name) for name in artists]),
    )


run(ArtistList)

docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_insert.py METASEP
from reactpy import component, html, run, use_state


@component
def ArtistList():
    artist_to_add, set_artist_to_add = use_state("")
    artists, set_artists = use_state([])

    def handle_change(event):
        set_artist_to_add(event["target"]["value"])

    def handle_click(event):
        if artist_to_add and artist_to_add not in artists:
            set_artists([*artists, artist_to_add])
            set_artist_to_add("")

    return html.div(
        html.h1("Inspiring sculptors:"),
        html.input({"value": artist_to_add, "on_change": handle_change}),
        html.button({"on_click": handle_click}, "add"),
        html.ul([html.li({"key": name}, name) for name in artists]),
    )


run(ArtistList)

docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/dict_update.py METASEP
from reactpy import component, html, run, use_state


@component
def Form():
    person, set_person = use_state(
        {
            "first_name": "Barbara",
            "last_name": "Hepworth",
            "email": "bhepworth@sculpture.com",
        }
    )

    def handle_first_name_change(event):
        set_person({**person, "first_name": event["target"]["value"]})

    def handle_last_name_change(event):
        set_person({**person, "last_name": event["target"]["value"]})

    def handle_email_change(event):
        set_person({**person, "email": event["target"]["value"]})

    return html.div(
        html.label(
            "First name: ",
            html.input(
                {"value": person["first_name"], "on_change": handle_first_name_change}
            ),
        ),
        html.label(
            "Last name: ",
            html.input(
                {"value": person["last_name"], "on_change": handle_last_name_change}
            ),
        ),
        html.label(
            "Email: ",
            html.input({"value": person["email"], "on_change": handle_email_change}),
        ),
        html.p(f"{person['first_name']} {person['last_name']} {person['email']}"),
    )


run(Form)

docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/dict_remove.py METASEP
from reactpy import component, html, run, use_state


@component
def Definitions():
    term_to_add, set_term_to_add = use_state(None)
    definition_to_add, set_definition_to_add = use_state(None)
    all_terms, set_all_terms = use_state({})

    def handle_term_to_add_change(event):
        set_term_to_add(event["target"]["value"])

    def handle_definition_to_add_change(event):
        set_definition_to_add(event["target"]["value"])

    def handle_add_click(event):
        if term_to_add and definition_to_add:
            set_all_terms({**all_terms, term_to_add: definition_to_add})
            set_term_to_add(None)
            set_definition_to_add(None)

    def make_delete_click_handler(term_to_delete):
        def handle_click(event):
            set_all_terms({t: d for t, d in all_terms.items() if t != term_to_delete})

        return handle_click

    return html.div(
        html.button({"on_click": handle_add_click}, "add term"),
        html.label(
            "Term: ",
            html.input({"value": term_to_add, "on_change": handle_term_to_add_change}),
        ),
        html.label(
            "Definition: ",
            html.input(
                {
                    "value": definition_to_add,
                    "on_change": handle_definition_to_add_change,
                }
            ),
        ),
        html.hr(),
        [
            html.div(
                {"key": term},
                html.button(
                    {"on_click": make_delete_click_handler(term)}, "delete term"
                ),
                html.dt(term),
                html.dd(definition),
            )
            for term, definition in all_terms.items()
        ],
    )


run(Definitions)

docs/source/guides/understanding-reactpy/_static/npm-download-trends.png METASEP

docs/source/guides/understanding-reactpy/_static/mvc-flow-diagram.svg METASEP
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   version="1.1"
   width="680"
   height="580"
   viewBox="-0.5 -0.5 680 580"
   content="&lt;mxfile host=&quot;app.diagrams.net&quot; modified=&quot;2020-09-07T18:34:20.858Z&quot; agent=&quot;5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36&quot; etag=&quot;IvUE9xI9CxZQnD7O0sJm&quot; version=&quot;13.6.6&quot; type=&quot;device&quot;&gt;&lt;diagram id=&quot;3GUrj3vU2Wc3lj3yRkW7&quot; name=&quot;Page-1&quot;&gt;7Zpdb9owFIZ/DZetkrgEuCy027R1XSWkddqdiU8Sqw5mjvnar5+d2CEhFEpbPkZBlUqO7WP7+D0PdpIG6iWzzwKP4u+cAGt4Dpk10E3D89wrz2voP4fMc0sLdXJDJCgxlRaGPv0Lxtg01jElkFYqSs6ZpKOqMeDDIQSyYsNC8Gm1WshZtdcRjqBm6AeY1a2PlMg4t7abzsL+BWgU255dx5Qk2FY2hjTGhE9LJnTbQD3Bucy/JbMeMB08G5e83adnSouBCRjKlzTAv+WFQ760viXebfT49/4J/cIXfu5lgtnYTLjh+Uz564ZcuVUBw0Fe4P8Z65F2e3wsKAhVdA/ThVlPUM5Zta72cZFma3qtKqjRzsot/Mj8zzocWMMdnvOxtGY1ocFyVWXLh2fNXqV7T/DxkICet6uKpzGV0B/lE5kqmSpbLBNmikPKWI8zLrK2iGBoh4Gyp1LwJyiV+EEbBmFRYuWAihFMQEiYPbs8brHoKluAJyDFXFUxDTzX6MQkim8upwvVFckUlxRn62Ej9KjwvNCC+mLksIU0WkcojRuajrAMYt3Jx5ZHcX0wfbSPUB+PoL46fRCTs0LQwQnSOUKF9BiF4dofl4H4iGppHpwn7iqgLEUahuRab+rUVcBwmtKgGtxqqJr1cINLmtBaFe6O30LYL4ILpLYv3BjaUujs/rAcOWsTwLCkk6r7VeE0PTxwmmXLbGml5naVllYkVUkUgGlV3hBucFRcW0cSiwhkzVG2usW037DgG/mwddJvQZQ8l7/2f9yrsge9qXhpumtp3OGBOuhUtIcZjYZamEomqkPU1flJ1Uni2hQklBDtoytAzQAPMn+Ouh7pCGcxb3YbzRtlYdp9FwdPUcaWkkw/ZZ91DDAnIeN/cf4oC3hNAj5LDOfSayNUUYz7NkEbLxduq9qEh2EKO5GcHdyZMdsxxmu9E2PQ8s/Hjhlj1/fMmNcwRg/R3JLxnJ0wZ8MuZYfMQXtjDjoz5zXMcd9rX+PteV/jXR2eOT9vfnw/s2ZVIp42a1Ztqd+bNWEIfrDyyEpanYHj/AesQWgDIl7KmmVHNWjtmDW2+8Pub1TJ7aR6W+VMnCId90+c/QEH7eNAdYLAqZ2DXguc2sls18A5igPVGTjr0vGkgbOP09QJAqd2c/e1wKndbt41cI7gNHUGzvp0PGXgXG3cYW+rpkcYpDx4gkJM2z9xzNb+gadUUq5lI3J+FPpiEMqVoEtjPNI+klmk39e6TNRAxqPLBAv9LxgLNu+KbD4VYNpXl7SICE7j7GGns/J2U/bR1ahQisqHp1ijR/Cukn35o07fvku2TLANJO5s/6RTXS5e6MoVuHgtDt3+Aw==&lt;/diagram&gt;&lt;/mxfile&gt;"
   id="svg4818"
   sodipodi:docname="mvc-flow-diagram.svg"
   inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
  <metadata
     id="metadata4822">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
        <dc:title></dc:title>
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <sodipodi:namedview
     pagecolor="#ffffff"
     bordercolor="#666666"
     borderopacity="1"
     objecttolerance="10"
     gridtolerance="10"
     guidetolerance="10"
     inkscape:pageopacity="0"
     inkscape:pageshadow="2"
     inkscape:window-width="1920"
     inkscape:window-height="1016"
     id="namedview4820"
     showgrid="true"
     inkscape:zoom="1.1798897"
     inkscape:cx="230.94611"
     inkscape:cy="244.6615"
     inkscape:window-x="0"
     inkscape:window-y="27"
     inkscape:window-maximized="1"
     inkscape:current-layer="svg4818"
     showguides="false"
     inkscape:snap-object-midpoints="false"
     inkscape:snap-bbox="true"
     inkscape:snap-center="false"
     inkscape:snap-text-baseline="true">
    <inkscape:grid
       type="xygrid"
       id="grid4962"
       spacingx="10"
       spacingy="10" />
  </sodipodi:namedview>
  <defs
     id="defs4706">
    <marker
       inkscape:stockid="Arrow2Mend"
       orient="auto"
       refY="0"
       refX="0"
       id="marker5531"
       style="overflow:visible"
       inkscape:isstock="true">
      <path
         id="path5529"
         style="fill:#000000;fill-opacity:0.96568627;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:0.96568627"
         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
         transform="scale(-0.6)"
         inkscape:connector-curvature="0" />
    </marker>
    <marker
       inkscape:stockid="Arrow2Mend"
       orient="auto"
       refY="0"
       refX="0"
       id="marker5527"
       style="overflow:visible"
       inkscape:isstock="true">
      <path
         id="path5525"
         style="fill:#000000;fill-opacity:0.96568627;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:0.96568627"
         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
         transform="scale(-0.6)"
         inkscape:connector-curvature="0" />
    </marker>
    <marker
       inkscape:stockid="Arrow2Mend"
       orient="auto"
       refY="0"
       refX="0"
       id="marker5523"
       style="overflow:visible"
       inkscape:isstock="true">
      <path
         id="path5521"
         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
         transform="scale(-0.6)"
         inkscape:connector-curvature="0" />
    </marker>
    <marker
       inkscape:stockid="Arrow2Mend"
       orient="auto"
       refY="0"
       refX="0"
       id="Arrow2Mend"
       style="overflow:visible"
       inkscape:isstock="true">
      <path
         id="path5224"
         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
         transform="scale(-0.6)"
         inkscape:connector-curvature="0" />
    </marker>
    <marker
       inkscape:stockid="Arrow1Mend"
       orient="auto"
       refY="0"
       refX="0"
       id="Arrow1Mend"
       style="overflow:visible"
       inkscape:isstock="true">
      <path
         id="path5206"
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
         inkscape:connector-curvature="0" />
    </marker>
    <marker
       inkscape:stockid="Arrow1Lend"
       orient="auto"
       refY="0"
       refX="0"
       id="marker5513"
       style="overflow:visible"
       inkscape:isstock="true">
      <path
         id="path5511"
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
         transform="matrix(-0.8,0,0,-0.8,-10,0)"
         inkscape:connector-curvature="0" />
    </marker>
    <marker
       inkscape:stockid="Arrow1Lend"
       orient="auto"
       refY="0"
       refX="0"
       id="marker5509"
       style="overflow:visible"
       inkscape:isstock="true">
      <path
         id="path5507"
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
         transform="matrix(-0.8,0,0,-0.8,-10,0)"
         inkscape:connector-curvature="0" />
    </marker>
    <marker
       inkscape:stockid="Arrow1Lend"
       orient="auto"
       refY="0"
       refX="0"
       id="Arrow1Lend"
       style="overflow:visible"
       inkscape:isstock="true">
      <path
         id="path5200"
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
         style="fill:#000000;fill-opacity:0.96568627;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:0.96568627"
         transform="matrix(-0.8,0,0,-0.8,-10,0)"
         inkscape:connector-curvature="0" />
    </marker>
    <marker
       inkscape:stockid="Arrow1Lstart"
       orient="auto"
       refY="0"
       refX="0"
       id="Arrow1Lstart"
       style="overflow:visible"
       inkscape:isstock="true">
      <path
         id="path5197"
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
         style="fill:#000000;fill-opacity:0.96568627;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:0.96568627"
         transform="matrix(0.8,0,0,0.8,10,0)"
         inkscape:connector-curvature="0" />
    </marker>
    <marker
       inkscape:stockid="Arrow1Lend"
       orient="auto"
       refY="0"
       refX="0"
       id="Arrow1Lend-9"
       style="overflow:visible"
       inkscape:isstock="true">
      <path
         inkscape:connector-curvature="0"
         id="path5200-3"
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
         style="fill:#000000;fill-opacity:0.96568627;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:0.96568627"
         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
    </marker>
  </defs>
  <rect
     style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
     id="rect1940"
     width="680"
     height="580"
     x="-0.5"
     y="-0.5" />
  <rect
     style="opacity:1;fill:#9fa8da;fill-opacity:0.99607843;stroke:none;stroke-width:13;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
     id="rect4966"
     width="600"
     height="200"
     x="39.5"
     y="39.5"
     ry="20" />
  <g
     id="g5077"
     transform="translate(30,40)">
    <g
       id="g5106">
      <rect
         style="opacity:1;fill:#4052b5;fill-opacity:1;stroke:none;stroke-width:13;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
         id="rect5002-9"
         width="200"
         height="100"
         x="59.5"
         y="49.5"
         ry="20" />
      <text
         xml:space="preserve"
         style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
         x="123.48081"
         y="109.51302"
         id="text5072"><tspan
           sodipodi:role="line"
           id="tspan5070"
           x="123.48081"
           y="109.51302"
           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:26.66666603px;font-family:monospace;-inkscape-font-specification:monospace;fill:#ffffff;fill-opacity:1">model</tspan></text>
    </g>
  </g>
  <g
     transform="translate(330,40)"
     id="g5077-0">
    <rect
       ry="20"
       y="49.5"
       x="59.5"
       height="100"
       width="200"
       id="rect5002-9-9"
       style="opacity:1;fill:#4052b5;fill-opacity:1;stroke:none;stroke-width:13;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
    <text
       id="text5072-2"
       y="109.51302"
       x="78.464844"
       style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
       xml:space="preserve"><tspan
         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:26.66666603px;font-family:monospace;-inkscape-font-specification:monospace;fill:#ffffff;fill-opacity:1"
         y="109.51302"
         x="78.464844"
         id="tspan5070-5"
         sodipodi:role="line">controller</tspan></text>
  </g>
  <text
     xml:space="preserve"
     style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none"
     x="49.5"
     y="69.5"
     id="text5110"><tspan
       sodipodi:role="line"
       id="tspan5108"
       x="49.5"
       y="69.5"
       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:24px;font-family:monospace;-inkscape-font-specification:monospace;fill:#ffffff;fill-opacity:1">server</tspan></text>
  <rect
     style="opacity:1;fill:#9fa8da;fill-opacity:0.99607843;stroke:none;stroke-width:13;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
     id="rect4966-5"
     width="600"
     height="200"
     x="39.5"
     y="339.5"
     ry="20" />
  <g
     id="g5077-9"
     transform="translate(30,340)">
    <g
       id="g5106-4">
      <rect
         style="opacity:1;fill:#4052b5;fill-opacity:1;stroke:none;stroke-width:13;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
         id="rect5002-9-6"
         width="200"
         height="100"
         x="59.5"
         y="49.5"
         ry="20" />
      <text
         xml:space="preserve"
         style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
         x="123.48081"
         y="109.51302"
         id="text5072-9"><tspan
           sodipodi:role="line"
           id="tspan5070-2"
           x="123.48081"
           y="109.51302"
           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:26.66666603px;font-family:monospace;-inkscape-font-specification:monospace;fill:#ffffff;fill-opacity:1">model</tspan></text>
    </g>
  </g>
  <g
     transform="translate(330,340)"
     id="g5077-0-2">
    <rect
       ry="20"
       y="49.5"
       x="59.5"
       height="100"
       width="200"
       id="rect5002-9-9-4"
       style="opacity:1;fill:#4052b5;fill-opacity:1;stroke:none;stroke-width:13;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
    <text
       id="text5072-2-7"
       y="109.5"
       x="129.5"
       style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
       xml:space="preserve"><tspan
         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:26.66666603px;font-family:monospace;-inkscape-font-specification:monospace;fill:#ffffff;fill-opacity:1"
         y="109.5"
         x="129.5"
         id="tspan5070-5-7"
         sodipodi:role="line">view</tspan></text>
  </g>
  <text
     xml:space="preserve"
     style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none"
     x="49.817509"
     y="370.17709"
     id="text5110-5"><tspan
       sodipodi:role="line"
       id="tspan5108-4"
       x="49.817509"
       y="370.17709"
       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:24px;font-family:monospace;-inkscape-font-specification:monospace;fill:#ffffff;fill-opacity:1">client</tspan></text>
  <path
     style="display:inline;fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker5523)"
     d="m 489.5,389.5 v -200"
     id="path5179"
     inkscape:connector-type="polyline"
     inkscape:connector-curvature="0" />
  <path
     style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Mend)"
     d="m 389.5,139.5 h -100"
     id="path5181"
     inkscape:connector-type="polyline"
     inkscape:connector-curvature="0" />
  <path
     style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:3.29999995;stroke-dasharray:none;stroke-opacity:0.96568627;marker-end:url(#marker5527)"
     d="m 189.5,189.5 v 200"
     id="path5183"
     inkscape:connector-type="polyline"
     inkscape:connector-curvature="0" />
  <path
     style="display:inline;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:3.29999995;stroke-dasharray:none;stroke-opacity:0.96568627;marker-end:url(#marker5531)"
     d="m 289.5,439.5 h 100"
     id="path5183-6"
     inkscape:connector-type="polyline"
     inkscape:connector-curvature="0" />
  <text
     xml:space="preserve"
     style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
     x="499.5"
     y="299.5"
     id="text5535"><tspan
       sodipodi:role="line"
       id="tspan5533"
       x="499.5"
       y="299.5"
       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:26.66666603px;font-family:monospace;-inkscape-font-specification:monospace">event</tspan></text>
  <text
     xml:space="preserve"
     style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
     x="199.5"
     y="299.5"
     id="text5535-8"><tspan
       sodipodi:role="line"
       id="tspan5533-0"
       x="199.5"
       y="299.5"
       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:26.66666603px;font-family:monospace;-inkscape-font-specification:monospace">sync</tspan></text>
  <text
     xml:space="preserve"
     style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
     x="289.5"
     y="189.5"
     id="text5535-8-2"><tspan
       sodipodi:role="line"
       id="tspan5533-0-1"
       x="289.5"
       y="189.5"
       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:26.66666603px;font-family:monospace;-inkscape-font-specification:monospace">change</tspan></text>
  <text
     xml:space="preserve"
     style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
     x="289.5"
     y="489.5"
     id="text5535-8-2-0"><tspan
       sodipodi:role="line"
       id="tspan5533-0-1-5"
       x="289.5"
       y="489.5"
       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:26.66666603px;font-family:monospace;-inkscape-font-specification:monospace">render</tspan></text>
</svg>

docs/source/guides/understanding-reactpy/_static/live-examples-in-docs.gif METASEP

docs/source/guides/understanding-reactpy/_static/idom-flow-diagram.svg METASEP
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   version="1.1"
   width="680"
   height="580"
   viewBox="-0.5 -0.5 680 580"
   content="&lt;mxfile host=&quot;app.diagrams.net&quot; modified=&quot;2020-09-07T18:34:20.858Z&quot; agent=&quot;5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36&quot; etag=&quot;IvUE9xI9CxZQnD7O0sJm&quot; version=&quot;13.6.6&quot; type=&quot;device&quot;&gt;&lt;diagram id=&quot;3GUrj3vU2Wc3lj3yRkW7&quot; name=&quot;Page-1&quot;&gt;7Zpdb9owFIZ/DZetkrgEuCy027R1XSWkddqdiU8Sqw5mjvnar5+d2CEhFEpbPkZBlUqO7WP7+D0PdpIG6iWzzwKP4u+cAGt4Dpk10E3D89wrz2voP4fMc0sLdXJDJCgxlRaGPv0Lxtg01jElkFYqSs6ZpKOqMeDDIQSyYsNC8Gm1WshZtdcRjqBm6AeY1a2PlMg4t7abzsL+BWgU255dx5Qk2FY2hjTGhE9LJnTbQD3Bucy/JbMeMB08G5e83adnSouBCRjKlzTAv+WFQ760viXebfT49/4J/cIXfu5lgtnYTLjh+Uz564ZcuVUBw0Fe4P8Z65F2e3wsKAhVdA/ThVlPUM5Zta72cZFma3qtKqjRzsot/Mj8zzocWMMdnvOxtGY1ocFyVWXLh2fNXqV7T/DxkICet6uKpzGV0B/lE5kqmSpbLBNmikPKWI8zLrK2iGBoh4Gyp1LwJyiV+EEbBmFRYuWAihFMQEiYPbs8brHoKluAJyDFXFUxDTzX6MQkim8upwvVFckUlxRn62Ej9KjwvNCC+mLksIU0WkcojRuajrAMYt3Jx5ZHcX0wfbSPUB+PoL46fRCTs0LQwQnSOUKF9BiF4dofl4H4iGppHpwn7iqgLEUahuRab+rUVcBwmtKgGtxqqJr1cINLmtBaFe6O30LYL4ILpLYv3BjaUujs/rAcOWsTwLCkk6r7VeE0PTxwmmXLbGml5naVllYkVUkUgGlV3hBucFRcW0cSiwhkzVG2usW037DgG/mwddJvQZQ8l7/2f9yrsge9qXhpumtp3OGBOuhUtIcZjYZamEomqkPU1flJ1Uni2hQklBDtoytAzQAPMn+Ouh7pCGcxb3YbzRtlYdp9FwdPUcaWkkw/ZZ91DDAnIeN/cf4oC3hNAj5LDOfSayNUUYz7NkEbLxduq9qEh2EKO5GcHdyZMdsxxmu9E2PQ8s/Hjhlj1/fMmNcwRg/R3JLxnJ0wZ8MuZYfMQXtjDjoz5zXMcd9rX+PteV/jXR2eOT9vfnw/s2ZVIp42a1Ztqd+bNWEIfrDyyEpanYHj/AesQWgDIl7KmmVHNWjtmDW2+8Pub1TJ7aR6W+VMnCId90+c/QEH7eNAdYLAqZ2DXguc2sls18A5igPVGTjr0vGkgbOP09QJAqd2c/e1wKndbt41cI7gNHUGzvp0PGXgXG3cYW+rpkcYpDx4gkJM2z9xzNb+gadUUq5lI3J+FPpiEMqVoEtjPNI+klmk39e6TNRAxqPLBAv9LxgLNu+KbD4VYNpXl7SICE7j7GGns/J2U/bR1ahQisqHp1ijR/Cukn35o07fvku2TLANJO5s/6RTXS5e6MoVuHgtDt3+Aw==&lt;/diagram&gt;&lt;/mxfile&gt;"
   id="svg4818"
   sodipodi:docname="reactpy-flow-diagram.svg"
   inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
  <metadata
     id="metadata4822">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
        <dc:title></dc:title>
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <sodipodi:namedview
     pagecolor="#ffffff"
     bordercolor="#666666"
     borderopacity="1"
     objecttolerance="10"
     gridtolerance="10"
     guidetolerance="10"
     inkscape:pageopacity="0"
     inkscape:pageshadow="2"
     inkscape:window-width="1920"
     inkscape:window-height="1016"
     id="namedview4820"
     showgrid="true"
     inkscape:zoom="1.1798897"
     inkscape:cx="295.35891"
     inkscape:cy="286.00243"
     inkscape:window-x="0"
     inkscape:window-y="27"
     inkscape:window-maximized="1"
     inkscape:current-layer="svg4818"
     showguides="false"
     inkscape:snap-object-midpoints="false"
     inkscape:snap-bbox="true"
     inkscape:snap-center="false"
     inkscape:snap-text-baseline="true">
    <inkscape:grid
       type="xygrid"
       id="grid4962"
       spacingx="10"
       spacingy="10" />
  </sodipodi:namedview>
  <defs
     id="defs4706">
    <marker
       inkscape:stockid="Arrow2Mend"
       orient="auto"
       refY="0"
       refX="0"
       id="marker5531"
       style="overflow:visible"
       inkscape:isstock="true">
      <path
         id="path5529"
         style="fill:#000000;fill-opacity:0.96568627;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:0.96568627"
         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
         transform="scale(-0.6)"
         inkscape:connector-curvature="0" />
    </marker>
    <marker
       inkscape:stockid="Arrow2Mend"
       orient="auto"
       refY="0"
       refX="0"
       id="marker5527"
       style="overflow:visible"
       inkscape:isstock="true">
      <path
         id="path5525"
         style="fill:#000000;fill-opacity:0.96568627;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:0.96568627"
         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
         transform="scale(-0.6)"
         inkscape:connector-curvature="0" />
    </marker>
    <marker
       inkscape:stockid="Arrow2Mend"
       orient="auto"
       refY="0"
       refX="0"
       id="marker5523"
       style="overflow:visible"
       inkscape:isstock="true">
      <path
         id="path5521"
         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
         transform="scale(-0.6)"
         inkscape:connector-curvature="0" />
    </marker>
    <marker
       inkscape:stockid="Arrow2Mend"
       orient="auto"
       refY="0"
       refX="0"
       id="Arrow2Mend"
       style="overflow:visible"
       inkscape:isstock="true">
      <path
         id="path5224"
         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
         d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
         transform="scale(-0.6)"
         inkscape:connector-curvature="0" />
    </marker>
    <marker
       inkscape:stockid="Arrow1Mend"
       orient="auto"
       refY="0"
       refX="0"
       id="Arrow1Mend"
       style="overflow:visible"
       inkscape:isstock="true">
      <path
         id="path5206"
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
         inkscape:connector-curvature="0" />
    </marker>
    <marker
       inkscape:stockid="Arrow1Lend"
       orient="auto"
       refY="0"
       refX="0"
       id="marker5513"
       style="overflow:visible"
       inkscape:isstock="true">
      <path
         id="path5511"
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
         transform="matrix(-0.8,0,0,-0.8,-10,0)"
         inkscape:connector-curvature="0" />
    </marker>
    <marker
       inkscape:stockid="Arrow1Lend"
       orient="auto"
       refY="0"
       refX="0"
       id="marker5509"
       style="overflow:visible"
       inkscape:isstock="true">
      <path
         id="path5507"
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
         transform="matrix(-0.8,0,0,-0.8,-10,0)"
         inkscape:connector-curvature="0" />
    </marker>
    <marker
       inkscape:stockid="Arrow1Lend"
       orient="auto"
       refY="0"
       refX="0"
       id="Arrow1Lend"
       style="overflow:visible"
       inkscape:isstock="true">
      <path
         id="path5200"
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
         style="fill:#000000;fill-opacity:0.96568627;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:0.96568627"
         transform="matrix(-0.8,0,0,-0.8,-10,0)"
         inkscape:connector-curvature="0" />
    </marker>
    <marker
       inkscape:stockid="Arrow1Lstart"
       orient="auto"
       refY="0"
       refX="0"
       id="Arrow1Lstart"
       style="overflow:visible"
       inkscape:isstock="true">
      <path
         id="path5197"
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
         style="fill:#000000;fill-opacity:0.96568627;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:0.96568627"
         transform="matrix(0.8,0,0,0.8,10,0)"
         inkscape:connector-curvature="0" />
    </marker>
    <marker
       inkscape:stockid="Arrow1Lend"
       orient="auto"
       refY="0"
       refX="0"
       id="Arrow1Lend-9"
       style="overflow:visible"
       inkscape:isstock="true">
      <path
         inkscape:connector-curvature="0"
         id="path5200-3"
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
         style="fill:#000000;fill-opacity:0.96568627;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:0.96568627"
         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
    </marker>
  </defs>
  <rect
     style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:5.73294544;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
     id="rect1077"
     width="680"
     height="580"
     x="-0.5"
     y="-0.5" />
  <rect
     style="opacity:1;fill:#9fa8da;fill-opacity:0.99607843;stroke:none;stroke-width:13;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
     id="rect4966"
     width="600"
     height="200"
     x="39.5"
     y="39.5"
     ry="20" />
  <g
     id="g5077"
     transform="translate(30,40)">
    <g
       id="g5106"
       style="opacity:1">
      <rect
         style="opacity:1;fill:#4052b5;fill-opacity:1;stroke:none;stroke-width:13;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
         id="rect5002-9"
         width="200"
         height="100"
         x="59.5"
         y="49.5"
         ry="20" />
      <text
         xml:space="preserve"
         style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
         x="109.5"
         y="109.5"
         id="text5072"><tspan
           sodipodi:role="line"
           id="tspan5070"
           x="109.5"
           y="109.5"
           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:26.66666603px;font-family:monospace;-inkscape-font-specification:monospace;fill:#ffffff;fill-opacity:1">layout</tspan></text>
    </g>
  </g>
  <g
     transform="translate(330,40)"
     id="g5077-0">
    <rect
       ry="20"
       y="49.5"
       x="59.5"
       height="100"
       width="200"
       id="rect5002-9-9"
       style="opacity:1;fill:#4052b5;fill-opacity:1;stroke:none;stroke-width:13;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
    <text
       id="text5072-2"
       y="109.5"
       x="89.5"
       style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
       xml:space="preserve"><tspan
         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:26.66666603px;font-family:monospace;-inkscape-font-specification:monospace;fill:#ffffff;fill-opacity:1"
         y="109.5"
         x="89.5"
         id="tspan5070-5"
         sodipodi:role="line">component</tspan></text>
  </g>
  <text
     xml:space="preserve"
     style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none"
     x="49.5"
     y="69.5"
     id="text5110"><tspan
       sodipodi:role="line"
       id="tspan5108"
       x="49.5"
       y="69.5"
       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:24px;font-family:monospace;-inkscape-font-specification:monospace;fill:#ffffff;fill-opacity:1">server</tspan></text>
  <rect
     style="opacity:1;fill:#9fa8da;fill-opacity:0.99607843;stroke:none;stroke-width:13;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
     id="rect4966-5"
     width="600"
     height="200"
     x="39.5"
     y="339.5"
     ry="20" />
  <g
     transform="translate(180,350)"
     id="g5077-0-2">
    <rect
       ry="20"
       y="49.5"
       x="59.5"
       height="100"
       width="200"
       id="rect5002-9-9-4"
       style="opacity:1;fill:#4052b5;fill-opacity:1;stroke:none;stroke-width:13;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
    <text
       id="text5072-2-7"
       y="109.5"
       x="129.5"
       style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
       xml:space="preserve"><tspan
         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:26.66666603px;font-family:monospace;-inkscape-font-specification:monospace;fill:#ffffff;fill-opacity:1"
         y="109.5"
         x="129.5"
         id="tspan5070-5-7"
         sodipodi:role="line">view</tspan></text>
  </g>
  <text
     xml:space="preserve"
     style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none"
     x="49.817509"
     y="370.17709"
     id="text5110-5"><tspan
       sodipodi:role="line"
       id="tspan5108-4"
       x="49.817509"
       y="370.17709"
       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:24px;font-family:monospace;-inkscape-font-specification:monospace;fill:#ffffff;fill-opacity:1">client</tspan></text>
  <path
     style="display:inline;fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker5523)"
     d="m 359.5,399.5 130,-210"
     id="path5179"
     inkscape:connector-type="polyline"
     inkscape:connector-curvature="0" />
  <path
     style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Mend)"
     d="m 389.5,139.5 h -100"
     id="path5181"
     inkscape:connector-type="polyline"
     inkscape:connector-curvature="0" />
  <path
     style="display:inline;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:3.29999995;stroke-dasharray:none;stroke-opacity:0.96568627;marker-end:url(#marker5527)"
     d="m 189.5,189.5 130,210"
     id="path5183"
     inkscape:connector-type="polyline"
     inkscape:connector-curvature="0" />
  <text
     xml:space="preserve"
     style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:0.99516913;stroke:none"
     x="439.5"
     y="299.5"
     id="text5535"><tspan
       sodipodi:role="line"
       id="tspan5533"
       x="439.5"
       y="299.5"
       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:26.66666603px;font-family:monospace;-inkscape-font-specification:monospace;fill:#000000;fill-opacity:0.99516913">event</tspan></text>
  <text
     xml:space="preserve"
     style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
     x="89.5"
     y="299.5"
     id="text5535-8"><tspan
       sodipodi:role="line"
       id="tspan5533-0"
       x="89.5"
       y="299.5"
       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:26.66666603px;font-family:monospace;-inkscape-font-specification:monospace;fill:#000000;fill-opacity:1">VDOM diff</tspan></text>
  <text
     xml:space="preserve"
     style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:0.98067633;stroke:none"
     x="309.5"
     y="189.5"
     id="text5535-8-2"><tspan
       sodipodi:role="line"
       id="tspan5533-0-1"
       x="309.5"
       y="189.5"
       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:26.66666603px;font-family:monospace;-inkscape-font-specification:monospace;fill:#000000;fill-opacity:0.98067633">VDOM</tspan></text>
</svg>

docs/source/guides/managing-state/when-and-how-to-reset-state/index.rst METASEP
.. _When to Reset State:

When and How to Reset State 🚧
==============================

.. note::

    Under construction 🚧

docs/source/guides/managing-state/simplifying-updates-with-reducers/index.rst METASEP
Simplifying Updates with Reducers 🚧
====================================

.. note::

    Under construction 🚧

docs/source/guides/managing-state/sharing-component-state/index.rst METASEP
Sharing Component State
=======================

.. note::

    Parts of this document are still under construction 🚧

Sometimes, you want the state of two components to always change together. To do it,
remove state from both of them, move it to their closest common parent, and then pass it
down to them via props. This is known as “lifting state up”, and it’s one of the most
common things you will do writing code with ReactPy.


Synced Inputs
-------------

In the code below the two input boxes are synchronized, this happens because they share
state. The state is shared via the parent component ``SyncedInputs``. Check the ``value``
and ``set_value`` variables.

.. reactpy:: _examples/synced_inputs


Filterable  List
----------------

In the example below the search input and the list of elements below share the
same state, the state represents the food name.

Note how the component ``Table`` gets called at each change of state. The
component is observing the state and reacting to state changes automatically,
just like it would do in React.

.. reactpy:: _examples/filterable_list

.. note::

    Try typing a food name in the search bar.

docs/source/guides/managing-state/how-to-structure-state/index.rst METASEP
.. _Structuring Your State:

How to Structure State 🚧
=========================

.. note::

    Under construction 🚧

docs/source/guides/managing-state/deeply-sharing-state-with-contexts/index.rst METASEP
Deeply Sharing State with Contexts 🚧
=====================================

.. note::

    Under construction 🚧

docs/source/guides/managing-state/combining-contexts-and-reducers/index.rst METASEP
Combining Contexts and Reducers 🚧
==================================

.. note::

    Under construction 🚧

docs/source/guides/getting-started/_static/shared-client-state-server-slider.gif METASEP

docs/source/guides/getting-started/_static/reactpy-in-jupyterlab.gif METASEP

docs/source/guides/getting-started/_static/logo-plotly.svg METASEP
<svg xmlns="http://www.w3.org/2000/svg" width="147" height="46" viewBox="0 0 147 46">
    <g fill="none" fill-rule="evenodd">
        <path fill="#3F4F75" d="M41.909 46H3.66C1.64 46 0 44.307 0 42.22V3.78C0 1.692 1.64 0 3.661 0H41.91c2.022 0 3.661 1.692 3.661 3.78v38.44c0 2.087-1.64 3.78-3.661 3.78"/>
        <path fill="#80CFBE" d="M11.383 17.956a1.263 1.263 0 0 1 .153.26 1.148 1.148 0 0 0-.433-.243c-.033-.072-.095-.133-.169-.147l-.022-.004a1.087 1.087 0 0 0-.065-.081c.191.052.377.124.536.215m-1.368 1.867c.117-.007.233-.03.344-.071.021.025.044.051.068.075a1.051 1.051 0 0 1-.436.027l.024-.03m.448-1.395a.62.62 0 0 0 .143.047.838.838 0 0 1 .007.107c-.081-.107-.228-.114-.337-.052l-.02-.08.164-.05c.013.01.027.02.043.028m-.717.143a1.102 1.102 0 0 0 .063.247c-.05-.055-.073-.137-.063-.247m1.412.65a35.186 35.186 0 0 0-.068-.133.803.803 0 0 0 .055-.122c.03.088.032.173.013.255m.718-1.553a3.586 3.586 0 0 0-.112-.074 2.06 2.06 0 0 0-.44-.431c-.718-.517-1.816-.567-2.338.252-.026.04-.05.082-.072.126a1.997 1.997 0 0 0-.652 1.15.251.251 0 0 0-.176.242c-.015.52.252.927.636 1.199.4.36.993.505 1.573.52.858.023 1.45-.447 1.716-1.07l.01-.013c.457-.6.51-1.433-.145-1.901M11.383 10.386a1.263 1.263 0 0 1 .153.26 1.142 1.142 0 0 0-.433-.243c-.033-.073-.095-.133-.169-.147l-.022-.005a1.08 1.08 0 0 0-.065-.08c.191.051.377.123.536.215m-1.368 1.867c.117-.007.233-.03.344-.072.021.026.044.051.068.075a1.02 1.02 0 0 1-.436.028l.024-.031m.448-1.394a.643.643 0 0 0 .143.046.833.833 0 0 1 .007.108c-.081-.107-.228-.115-.337-.052l-.02-.08.164-.05c.013.01.027.02.043.028M9.746 11c.005.033.01.066.019.099.012.05.027.099.044.149-.05-.055-.073-.137-.063-.248m1.412.65l-.068-.132c.022-.04.04-.08.055-.122.03.088.032.173.013.254m.718-1.552a2.91 2.91 0 0 0-.112-.075 2.048 2.048 0 0 0-.44-.43c-.718-.518-1.816-.567-2.338.252-.026.04-.05.082-.072.125a1.997 1.997 0 0 0-.652 1.15.251.251 0 0 0-.176.242c-.015.52.252.927.636 1.2.4.359.993.504 1.573.52.858.023 1.45-.447 1.716-1.07l.01-.013c.457-.6.51-1.433-.145-1.901M28.02 17.956a1.263 1.263 0 0 1 .153.26 1.148 1.148 0 0 0-.433-.243c-.033-.072-.095-.133-.169-.147l-.022-.004a1.087 1.087 0 0 0-.065-.081c.192.052.377.124.536.215m-1.368 1.867c.117-.007.233-.03.344-.071.022.025.044.051.068.075a1.051 1.051 0 0 1-.436.027l.024-.03m.448-1.395a.62.62 0 0 0 .144.047.838.838 0 0 1 .006.107c-.08-.107-.228-.114-.337-.052l-.02-.08.164-.05c.013.01.027.02.043.028m-.717.143a1.102 1.102 0 0 0 .063.247c-.05-.055-.073-.137-.063-.247m1.412.65a35.186 35.186 0 0 0-.068-.133.803.803 0 0 0 .056-.122c.029.088.031.173.012.255m.718-1.553a3.586 3.586 0 0 0-.111-.074 2.06 2.06 0 0 0-.441-.431c-.717-.517-1.816-.567-2.338.252-.025.04-.05.082-.072.126a1.994 1.994 0 0 0-.652 1.15.251.251 0 0 0-.176.242c-.015.52.252.927.636 1.199.4.36.994.505 1.573.52.858.023 1.45-.447 1.716-1.07l.01-.013c.457-.6.51-1.433-.145-1.901M28.02 10.386a1.263 1.263 0 0 1 .153.26 1.142 1.142 0 0 0-.433-.243c-.033-.073-.095-.133-.169-.147l-.022-.005a1.08 1.08 0 0 0-.065-.08c.192.051.377.123.536.215m-1.368 1.867c.117-.007.233-.03.344-.072.022.026.044.051.068.075a1.02 1.02 0 0 1-.436.028l.024-.031m.448-1.394a.643.643 0 0 0 .144.046.833.833 0 0 1 .006.108c-.08-.107-.228-.115-.337-.052l-.02-.08.164-.05c.013.01.027.02.043.028m-.717.142a1.66 1.66 0 0 0 .063.247c-.05-.054-.073-.136-.063-.247m1.412.65l-.068-.132c.022-.04.04-.08.056-.122.029.088.031.173.012.254m.718-1.552a2.91 2.91 0 0 0-.111-.075 2.048 2.048 0 0 0-.441-.43c-.717-.518-1.816-.567-2.338.252-.025.04-.05.082-.072.125a1.994 1.994 0 0 0-.652 1.15.251.251 0 0 0-.176.242c-.015.52.252.927.636 1.2.4.359.994.504 1.573.52.858.023 1.45-.447 1.716-1.07l.01-.013c.457-.6.51-1.433-.145-1.901"/>
        <path fill="#FFF" d="M10.656 24.81c-1.116 0-2.02.92-2.02 2.056v8.221c0 1.135.904 2.055 2.02 2.055s2.02-.92 2.02-2.055v-8.221c0-1.135-.904-2.055-2.02-2.055M26.818 24.81c-1.116 0-2.02.92-2.02 2.056v8.221c0 1.135.904 2.055 2.02 2.055 1.115 0 2.02-.92 2.02-2.055v-8.221c0-1.135-.905-2.055-2.02-2.055M18.737 16.59c-1.116 0-2.02.92-2.02 2.054v16.443c0 1.135.904 2.055 2.02 2.055s2.02-.92 2.02-2.055V18.644c0-1.135-.904-2.055-2.02-2.055M34.899 16.59c-1.116 0-2.02.92-2.02 2.054v16.443c0 1.135.904 2.055 2.02 2.055 1.115 0 2.02-.92 2.02-2.055V18.644c0-1.135-.905-2.055-2.02-2.055"/>
        <path fill="#80CFBE" d="M36.174 12.043c-.12.187-.263.316-.416.394.1-.156.198-.342.283-.546a2.23 2.23 0 0 0 .056-.126c.064-.058.1-.144.075-.227.002-.003.002-.007.003-.01.126-.094.215-.268.248-.455l.021-.033c.01.351-.078.703-.27 1.003m-2.656-.673a2.518 2.518 0 0 1-.009-.257c.079.295.198.584.32.837.07.188.16.386.274.53a1.13 1.13 0 0 1-.165-.127c-.269-.25-.386-.627-.42-.983m1.384-.405a3.724 3.724 0 0 1 .01-.564c.024.01.048.021.073.036l.039.027c.034.178.023.421-.122.5m1.823-.785a.407.407 0 0 0-.16-.31 2.293 2.293 0 0 0-.682-.717c-.383-.255-.815-.38-1.085-.047a3.714 3.714 0 0 0-.304-.015c-.062-.001-.119.004-.173.012-.322-.127-.666-.13-.815.282a1.743 1.743 0 0 0-.093.507.704.704 0 0 0-.158.164c-.313.449-.268 1.24-.144 1.74.21.852 1.117 1.522 1.903 1.27a.462.462 0 0 0 .184-.074c.42.079.878-.062 1.243-.507.545-.663.601-1.56.284-2.305M20.018 12.043c-.12.187-.264.316-.417.394.101-.156.198-.342.284-.546.02-.041.038-.083.055-.126.064-.058.1-.144.076-.227a.35.35 0 0 1 .003-.01c.126-.094.214-.268.248-.455l.02-.033c.01.351-.077.703-.27 1.003m-2.655-.673a2.518 2.518 0 0 1-.01-.257c.08.295.198.584.32.837.07.188.16.386.275.53a1.13 1.13 0 0 1-.165-.127c-.269-.25-.387-.627-.42-.983m1.384-.405a3.665 3.665 0 0 1 .01-.564c.023.01.047.021.072.036.014.008.026.017.04.027.033.178.022.421-.122.5m1.822-.785a.407.407 0 0 0-.159-.31 2.293 2.293 0 0 0-.683-.717c-.382-.255-.815-.38-1.084-.047a3.714 3.714 0 0 0-.304-.015c-.062-.001-.12.004-.173.012-.323-.127-.667-.13-.816.282a1.758 1.758 0 0 0-.093.507.704.704 0 0 0-.158.164c-.312.449-.268 1.24-.144 1.74.21.852 1.118 1.522 1.904 1.27a.462.462 0 0 0 .184-.074c.42.079.877-.062 1.243-.507.544-.663.6-1.56.283-2.305"/>
        <g fill="#3F4F75">
            <path d="M74.759 32.856a1.862 1.862 0 0 1-1.852-1.873V12.26c0-1.035.829-1.873 1.852-1.873 1.022 0 1.851.838 1.851 1.873v18.723a1.862 1.862 0 0 1-1.851 1.873M113.644 32.856a1.862 1.862 0 0 1-1.851-1.873V12.26c0-1.035.829-1.873 1.851-1.873 1.023 0 1.852.838 1.852 1.873v18.723a1.862 1.862 0 0 1-1.852 1.873M65.5 25.026c0 2.256-1.658 4.085-3.704 4.085-2.045 0-3.703-1.83-3.703-4.085v-3.064c0-2.256 1.658-4.085 3.703-4.085 2.046 0 3.704 1.829 3.704 4.085v3.064zm-3.704-10.894a6.953 6.953 0 0 0-3.842 1.163 1.852 1.852 0 0 0-1.713-1.163 1.862 1.862 0 0 0-1.851 1.872v18.724c0 1.034.829 1.873 1.851 1.873a1.862 1.862 0 0 0 1.852-1.873V31.78a6.937 6.937 0 0 0 3.703 1.076c4.091 0 7.407-3.593 7.407-8.025v-2.675c0-4.431-3.316-8.024-7.407-8.024zM91.424 25.026c0 2.256-1.658 4.085-3.704 4.085-2.045 0-3.703-1.83-3.703-4.085v-3.064c0-2.256 1.658-4.085 3.703-4.085 2.046 0 3.704 1.829 3.704 4.085v3.064zM87.72 14.132c-4.09 0-7.406 3.593-7.406 8.024v2.675c0 4.432 3.316 8.025 7.406 8.025 4.091 0 7.407-3.593 7.407-8.025v-2.675c0-4.431-3.316-8.024-7.407-8.024zM106.238 29.111c-2.042 0-3.704-1.68-3.704-3.745v-7.49h3.704a1.862 1.862 0 0 0 1.851-1.872 1.862 1.862 0 0 0-1.851-1.872h-3.704v-1.873a1.862 1.862 0 0 0-1.851-1.872 1.862 1.862 0 0 0-1.852 1.872v13.107c0 4.13 3.323 7.49 7.407 7.49a1.862 1.862 0 0 0 1.851-1.873 1.862 1.862 0 0 0-1.851-1.872M132.247 14.284a1.844 1.844 0 0 0-2.432.983l-3.84 9.056-3.839-9.056a1.844 1.844 0 0 0-2.432-.983 1.88 1.88 0 0 0-.972 2.458l5.229 12.333-.622 1.467a3.71 3.71 0 0 1-3.335 2.308 1.864 1.864 0 0 0-1.809 1.917 1.862 1.862 0 0 0 1.85 1.828h.045a7.41 7.41 0 0 0 6.661-4.597l6.468-15.256a1.88 1.88 0 0 0-.972-2.458"/>
        </g>
    </g>
</svg>

docs/source/guides/getting-started/_static/logo-jupyter.svg METASEP
<svg width="189" height="51" viewBox="0 0 189 51" version="2.0" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:figma="http://www.figma.com/figma/ns">
<title>logo.svg</title>
<desc>Created using Figma 0.90</desc>
<g id="Canvas" transform="translate(-1638 -2093)" figma:type="canvas">
<g id="logo" style="mix-blend-mode:normal;" figma:type="group">
<g id="Group" style="mix-blend-mode:normal;" figma:type="group">
<g id="g" style="mix-blend-mode:normal;" figma:type="group">
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path0 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path0_fill" transform="translate(1688.87 2106.23)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path1 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path1_fill" transform="translate(1705.38 2106.19)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path2 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path2_fill" transform="translate(1730.18 2105.67)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path3 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path3_fill" transform="translate(1752.94 2106.21)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path4 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path4_fill" transform="translate(1775.8 2100.04)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path5 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path5_fill" transform="translate(1791.75 2105.71)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path6 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path6_fill" transform="translate(1815.77 2105.72)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
</g>
</g>
<g id="g" style="mix-blend-mode:normal;" figma:type="group">
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path7 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path7_fill" transform="translate(1669.3 2093.31)" fill="#767677" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path8 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path8_fill" transform="translate(1639.74 2123.98)" fill="#F37726" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path9 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path9_fill" transform="translate(1639.73 2097.48)" fill="#F37726" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path10 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path10_fill" transform="translate(1639.8 2135.81)" fill="#989798" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path11 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path11_fill" transform="translate(1638.36 2098.06)" fill="#6F7070" style="mix-blend-mode:normal;"/>
</g>
</g>
</g>
</g>
</g>
<defs>
<path id="path0_fill" d="M 5.62592 17.9276C 5.62592 23.0737 5.23342 24.7539 4.18673 25.9942C 3.04416 27.0501 1.54971 27.6341 0 27.6304L 0.392506 30.6916C 2.79452 30.7249 5.12302 29.8564 6.92556 28.255C 8.80086 26.3021 9.45504 23.6015 9.45504 19.4583L 9.45504 0L 5.6172 0L 5.6172 17.954L 5.62592 17.9276Z"/>
<path id="path1_fill" d="M 17.7413 15.6229C 17.7413 17.8397 17.7413 19.7925 17.9157 21.4727L 14.5576 21.4727L 14.3396 17.954L 14.2523 17.954C 13.5454 19.1838 12.5262 20.2013 11.2997 20.9017C 10.0732 21.6022 8.68377 21.9602 7.27445 21.9389C 3.95995 21.9389 0 20.074 0 12.5441L 0 0L 3.83784 0L 3.83784 11.9019C 3.83784 15.9836 5.05897 18.7281 8.53919 18.7281C 9.63092 18.708 10.6925 18.3634 11.5908 17.7374C 12.4892 17.1115 13.1844 16.2321 13.5894 15.2095C 13.8222 14.57 13.9404 13.8938 13.9383 13.2126L 13.9383 0.0175934L 17.7762 0.0175934L 17.7762 15.6229L 17.7413 15.6229Z"/>
<path id="path2_fill" d="M 0.174447 7.53632C 0.174447 4.79175 0.0872236 2.57499 0 0.498968L 3.44533 0.498968L 3.61978 4.17598L 3.707 4.17598C 4.46074 2.85853 5.55705 1.77379 6.87754 1.03893C 8.19802 0.304077 9.69248 -0.0529711 11.1995 0.0063534C 16.2934 0.0063534 20.1312 4.4047 20.1312 10.9142C 20.1312 18.6289 15.5171 22.4379 10.5366 22.4379C 9.25812 22.492 7.98766 22.2098 6.84994 21.6192C 5.71222 21.0285 4.74636 20.1496 4.04718 19.0688L 3.95995 19.0688L 3.95995 30.7244L 0.165725 30.7244L 0.165725 7.50113L 0.174447 7.53632ZM 3.96868 13.2542C 3.97869 13.7891 4.03708 14.3221 4.14312 14.8464C 4.45574 16.1467 5.19222 17.3035 6.23449 18.1313C 7.27677 18.9592 8.56446 19.4101 9.89116 19.4118C 13.9471 19.4118 16.2934 16.0427 16.2934 11.1254C 16.2934 6.82378 14.0692 3.14677 10.022 3.14677C 8.66089 3.18518 7.35158 3.68134 6.30219 4.55638C 5.25279 5.43143 4.52354 6.63513 4.23035 7.97615C 4.07662 8.49357 3.98869 9.02858 3.96868 9.56835L 3.96868 13.2454L 3.96868 13.2542Z"/>
<path id="path3_fill" d="M 4.16057 0L 8.7747 12.676C 9.25443 14.0923 9.77777 15.7813 10.1267 17.0744L 10.2139 17.0744C 10.6064 15.7901 11.0425 14.1451 11.5659 12.5969L 15.7526 0.00879669L 19.8085 0.00879669L 14.0604 15.3062C 11.3129 22.6603 9.44632 26.434 6.82961 28.7388C 5.50738 29.9791 3.88672 30.8494 2.12826 31.2634L 1.1688 27.9823C 2.39912 27.5689 3.53918 26.9208 4.52691 26.0734C 5.92259 24.8972 7.02752 23.4094 7.75418 21.7278C 7.90932 21.4374 8.01266 21.1218 8.05946 20.7954C 8.02501 20.4436 7.93674 20.0994 7.79779 19.7749L 0 0.00879669L 4.18673 0.00879669L 4.16057 0Z"/>
<path id="path4_fill" d="M 7.0215 0L 7.0215 6.15768L 12.5079 6.15768L 12.5079 9.13096L 7.0215 9.13096L 7.0215 20.6898C 7.0215 23.3288 7.7629 24.8594 9.89988 24.8594C 10.6496 24.8712 11.3975 24.7824 12.1241 24.5955L 12.2985 27.5248C 11.207 27.9125 10.0534 28.0915 8.89681 28.0526C 8.1298 28.1 7.36177 27.9782 6.64622 27.6956C 5.93068 27.413 5.28484 26.9765 4.75369 26.4164C 3.66339 25.2641 3.27089 23.3552 3.27089 20.8306L 3.27089 9.13096L 0 9.13096L 0 6.15768L 3.27089 6.15768L 3.27089 1.01162L 7.0215 0Z"/>
<path id="path5_fill" d="M 3.6285 11.9283C 3.71573 17.2063 7.03022 19.3791 10.8593 19.3791C 12.8612 19.4425 14.8527 19.0642 16.6946 18.2707L 17.3488 21.0593C 15.1419 21.994 12.7638 22.4467 10.3709 22.3876C 3.88145 22.3876 0 18.042 0 11.5676C 0 5.09328 3.75062 0 9.89116 0C 16.7731 0 18.6135 6.15768 18.6135 10.1074C 18.607 10.7165 18.5634 11.3246 18.4827 11.9283L 3.65467 11.9283L 3.6285 11.9283ZM 14.8716 9.13976C 14.9152 6.65909 13.8686 2.79735 9.55971 2.79735C 5.67826 2.79735 3.98612 6.43038 3.68084 9.13976L 14.8803 9.13976L 14.8716 9.13976Z"/>
<path id="path6_fill" d="M 0.174447 7.17854C 0.174447 4.65389 0.130835 2.48111 0 0.484261L 3.35811 0.484261L 3.48894 4.69787L 3.66339 4.69787C 4.62285 1.81256 6.93428 0.00044283 9.49865 0.00044283C 9.8663 -0.0049786 10.233 0.0394012 10.5889 0.132393L 10.5889 3.80941C 10.1593 3.71494 9.72029 3.67067 9.28059 3.67746C 6.57666 3.67746 4.66646 5.76227 4.14312 8.68277C 4.03516 9.28384 3.97681 9.89289 3.96867 10.5037L 3.96867 21.9394L 0.174447 21.9394L 0.174447 7.17854Z"/>
<path id="path7_fill" d="M 5.89353 2.844C 5.91889 3.43165 5.77085 4.01367 5.46815 4.51645C 5.16545 5.01922 4.72168 5.42015 4.19299 5.66851C 3.6643 5.91688 3.07444 6.00151 2.49805 5.91171C 1.92166 5.8219 1.38463 5.5617 0.954898 5.16401C 0.52517 4.76633 0.222056 4.24903 0.0839037 3.67757C -0.0542483 3.10611 -0.02123 2.50617 0.178781 1.95364C 0.378793 1.4011 0.736809 0.920817 1.20754 0.573538C 1.67826 0.226259 2.24055 0.0275919 2.82326 0.00267229C 3.60389 -0.0307115 4.36573 0.249789 4.94142 0.782551C 5.51711 1.31531 5.85956 2.05676 5.89353 2.844Z"/>
<path id="path8_fill" d="M 18.2646 7.13411C 10.4145 7.13411 3.55872 4.2576 0 0C 1.32539 3.8204 3.79556 7.13081 7.0686 9.47303C 10.3417 11.8152 14.2557 13.0734 18.269 13.0734C 22.2823 13.0734 26.1963 11.8152 29.4694 9.47303C 32.7424 7.13081 35.2126 3.8204 36.538 0C 32.9705 4.2576 26.1148 7.13411 18.2646 7.13411Z"/>
<path id="path9_fill" d="M 18.2733 5.93931C 26.1235 5.93931 32.9793 8.81583 36.538 13.0734C 35.2126 9.25303 32.7424 5.94262 29.4694 3.6004C 26.1963 1.25818 22.2823 0 18.269 0C 14.2557 0 10.3417 1.25818 7.0686 3.6004C 3.79556 5.94262 1.32539 9.25303 0 13.0734C 3.56745 8.82463 10.4232 5.93931 18.2733 5.93931Z"/>
<path id="path10_fill" d="M 7.42789 3.58338C 7.46008 4.3243 7.27355 5.05819 6.89193 5.69213C 6.51031 6.32607 5.95075 6.83156 5.28411 7.1446C 4.61747 7.45763 3.87371 7.56414 3.14702 7.45063C 2.42032 7.33712 1.74336 7.0087 1.20184 6.50695C 0.660328 6.0052 0.27861 5.35268 0.105017 4.63202C -0.0685757 3.91135 -0.0262361 3.15494 0.226675 2.45856C 0.479587 1.76217 0.931697 1.15713 1.52576 0.720033C 2.11983 0.282935 2.82914 0.0334395 3.56389 0.00313344C 4.54667 -0.0374033 5.50529 0.316706 6.22961 0.987835C 6.95393 1.65896 7.38484 2.59235 7.42789 3.58338L 7.42789 3.58338Z"/>
<path id="path11_fill" d="M 2.27471 4.39629C 1.84363 4.41508 1.41671 4.30445 1.04799 4.07843C 0.679268 3.8524 0.385328 3.52114 0.203371 3.12656C 0.0214136 2.73198 -0.0403798 2.29183 0.0258116 1.86181C 0.0920031 1.4318 0.283204 1.03126 0.575213 0.710883C 0.867222 0.39051 1.24691 0.164708 1.66622 0.0620592C 2.08553 -0.0405897 2.52561 -0.0154714 2.93076 0.134235C 3.33591 0.283941 3.68792 0.551505 3.94222 0.90306C 4.19652 1.25462 4.34169 1.67436 4.35935 2.10916C 4.38299 2.69107 4.17678 3.25869 3.78597 3.68746C 3.39516 4.11624 2.85166 4.37116 2.27471 4.39629L 2.27471 4.39629Z"/>
</defs>
</svg>

docs/source/guides/getting-started/_static/logo-django.svg METASEP
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 12.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 51448)  -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd" [
	<!ENTITY ns_svg "http://www.w3.org/2000/svg">
	<!ENTITY ns_xlink "http://www.w3.org/1999/xlink">
]>
<svg  version="1.0" id="Layer_1" xmlns="&ns_svg;" xmlns:xlink="&ns_xlink;" width="436.505" height="152.503"
	 viewBox="0 0 436.505 152.503" overflow="visible" enable-background="new 0 0 436.505 152.503" xml:space="preserve">
<g>
	<g>
		<path fill="#092E20" d="M51.464,0h23.872v110.496c-12.246,2.325-21.237,3.255-31.002,3.255C15.191,113.75,0,100.576,0,75.308
			c0-24.337,16.122-40.147,41.078-40.147c3.875,0,6.82,0.309,10.386,1.239V0z M51.464,55.62c-2.79-0.929-5.115-1.239-8.06-1.239
			c-12.091,0-19.067,7.441-19.067,20.461c0,12.712,6.666,19.687,18.912,19.687c2.634,0,4.805-0.155,8.215-0.619V55.62z"/>
		<path fill="#092E20" d="M113.312,36.865v55.338c0,19.067-1.395,28.212-5.58,36.118c-3.876,7.597-8.992,12.401-19.532,17.672
			l-22.167-10.541c10.541-4.96,15.656-9.299,18.911-15.967c3.411-6.82,4.497-14.726,4.497-35.497V36.865H113.312z M89.441,0.127
			h23.871v24.492H89.441V0.127z"/>
		<path fill="#092E20" d="M127.731,42.29c10.542-4.959,20.617-7.129,31.623-7.129c12.246,0,20.306,3.254,23.872,9.61
			c2.014,3.565,2.634,8.215,2.634,18.137v48.517c-10.697,1.55-24.182,2.636-34.102,2.636c-19.996,0-28.988-6.978-28.988-22.478
			c0-16.742,11.936-24.492,41.234-26.973v-5.27c0-4.34-2.17-5.889-8.216-5.889c-8.835,0-18.756,2.48-28.058,7.286V42.29z
			 M165.089,80.268c-15.812,1.55-20.927,4.031-20.927,10.231c0,4.65,2.946,6.82,9.456,6.82c3.566,0,6.82-0.309,11.471-1.084V80.268z
			"/>
		<path fill="#092E20" d="M197.487,40.585c14.105-3.72,25.731-5.424,37.512-5.424c12.246,0,21.082,2.789,26.354,8.215
			c4.96,5.114,6.509,10.694,6.509,22.632v46.812H243.99V66.938c0-9.145-3.1-12.557-11.625-12.557c-3.255,0-6.2,0.31-11.007,1.705
			v56.734h-23.871V40.585z"/>
		<path fill="#092E20" d="M277.142,125.842c8.372,4.34,16.742,6.354,25.577,6.354c15.655,0,22.321-6.354,22.321-21.546
			c0-0.155,0-0.31,0-0.465c-4.65,2.324-9.301,3.255-15.5,3.255c-20.927,0-34.26-13.796-34.26-35.652
			c0-27.129,19.688-42.473,54.564-42.473c10.232,0,19.688,1.084,31.159,3.409l-8.174,17.219c-6.356-1.24-0.509-0.166-5.312-0.631
			v2.481l0.309,10.074l0.154,13.022c0.155,3.254,0.155,6.51,0.311,9.765c0,2.945,0,4.341,0,6.511c0,20.462-1.705,30.072-6.82,37.977
			c-7.441,11.627-20.307,17.362-38.598,17.362c-9.301,0-17.36-1.395-25.732-4.651V125.842z M324.576,54.536c-0.31,0-0.619,0-0.774,0
			h-1.706c-4.649-0.155-10.074,1.084-13.796,3.409c-5.734,3.256-8.681,9.147-8.681,17.517c0,11.937,5.892,18.757,16.432,18.757
			c3.255,0,5.891-0.621,8.99-1.55v-1.706v-6.509c0-2.79-0.154-5.892-0.154-9.146l-0.154-11.005l-0.156-7.906V54.536z"/>
		<path fill="#092E20" d="M398.062,34.85c23.871,0,38.443,15.037,38.443,39.373c0,24.958-15.19,40.614-39.373,40.614
			c-23.873,0-38.599-15.037-38.599-39.218C358.534,50.505,373.726,34.85,398.062,34.85z M397.595,95.614
			c9.147,0,14.573-7.596,14.573-20.772c0-13.02-5.271-20.771-14.415-20.771c-9.457,0-14.884,7.597-14.884,20.771
			C382.87,88.019,388.296,95.614,397.595,95.614z"/>
	</g>
</g>
</svg>

docs/source/guides/getting-started/_static/embed-doc-ex.html METASEP
<div id="reactpy-app" />
<script type="module">
  import { mountLayoutWithWebSocket } from "https://esm.sh/@reactpy/client@0.33.3";
  mountLayoutWithWebSocket(
    document.getElementById("reactpy-app"),
    "wss://reactpy.dev/_reactpy/stream?view_id=todo"
  );
</script>

docs/source/guides/creating-interfaces/your-first-components/index.rst METASEP
Your First Components
=====================

As we learned :ref:`earlier <HTML with ReactPy>` we can use ReactPy to make rich structured
documents out of standard HTML elements. As these documents become larger and more
complex though, working with these tiny UI elements can become difficult. When this
happens, ReactPy allows you to group these elements together info "components". These
components can then be reused throughout your application.


Defining a Component
--------------------

At their core, components are just normal Python functions that return HTML. To define a
component you just need to add a ``@component`` `decorator
<https://realpython.com/primer-on-python-decorators/>`__ to a function. Functions
decorator in this way are known as **render function** and, by convention, we name them
like classes - with ``CamelCase``. So consider what we would do if we wanted to write,
and then :ref:`display <Running ReactPy>` a ``Photo`` component:

.. reactpy:: _examples/simple_photo

.. warning::

    If we had not decorated our ``Photo``'s render function with the ``@component``
    decorator, the server would start, but as soon as we tried to view the page it would
    be blank. The servers logs would then indicate:

    .. code-block:: text

        TypeError: Expected a ComponentType, not dict.


Using a Component
-----------------

Having defined our ``Photo`` component we can now nest it inside of other components. We
can define a "parent" ``Gallery`` component that returns one or more ``Profile``
components. This is part of what makes components so powerful - you can define a
component once and use it wherever and however you need to:

.. reactpy:: _examples/nested_photos


Return a Single Root Element
----------------------------

Components must return a "single root element". That one root element may have children,
but you cannot for example, return a list of element from a component and expect it to
be rendered correctly. If you want to return multiple elements you must wrap them in
something like a :func:`html.div <reactpy.html.div>`:

.. reactpy:: _examples/wrap_in_div

If don't want to add an extra ``div`` you can use a "fragment" instead with the
:func:`html._ <reactpy.html._>` function:

.. reactpy:: _examples/wrap_in_fragment

Fragments allow you to group elements together without leaving any trace in the UI. For
example, the first code sample written with ReactPy will produce the second HTML code
block:

.. grid:: 1 2 2 2
    :margin: 0
    :padding: 0

    .. grid-item::

        .. testcode::

            from reactpy import html

            html.ul(
                html._(
                    html.li("Group 1 Item 1"),
                    html.li("Group 1 Item 2"),
                    html.li("Group 1 Item 3"),
                ),
                html._(
                    html.li("Group 2 Item 1"),
                    html.li("Group 2 Item 2"),
                    html.li("Group 2 Item 3"),
                )
            )

    .. grid-item::

        .. code-block:: html

            <ul>
              <li>Group 1 Item 1</li>
              <li>Group 1 Item 2</li>
              <li>Group 1 Item 3</li>
              <li>Group 2 Item 1</li>
              <li>Group 2 Item 2</li>
              <li>Group 2 Item 3</li>
            </ul>



Parametrizing Components
------------------------

Since components are just regular functions, you can add parameters to them. This allows
parent components to pass information to child components. Where standard HTML elements
are parametrized by dictionaries, since components behave like typical functions you can
give them positional and keyword arguments as you would normally:

.. reactpy:: _examples/parametrized_photos


Conditional Rendering
---------------------

Your components will often need to display different things depending on different
conditions. Let's imagine that we had a basic todo list where only some of the items
have been completed. Below we have a basic implementation for such a list except that
the ``Item`` component doesn't change based on whether it's ``done``:

.. reactpy:: _examples/todo_list

Let's imagine that we want to add a ✔ to the items which have been marked ``done=True``.
One way to do this might be to write an ``if`` statement where we return one ``li``
element if the item is ``done`` and a different one if it's not:

.. reactpy:: _examples/bad_conditional_todo_list

As you can see this accomplishes our goal! However, notice how similar ``html.li(name, "
✔")`` and ``html.li(name)`` are. While in this case it isn't especially harmful, we
could make our code a little easier to read and maintain by using an "inline" ``if``
statement.

.. reactpy:: _examples/good_conditional_todo_list

docs/source/guides/creating-interfaces/rendering-data/index.rst METASEP
Rendering Data
==============

Frequently you need to construct a number of similar components from a collection of
data. Let's imagine that we want to create a todo list that can be ordered and filtered
on the priority of each item in the list. To start, we'll take a look at the kind of
view we'd like to display:

.. code-block:: html

    <ul>
        <li>Make breakfast (important)</li>
        <li>Feed the dog (important)</li>
        <li>Do laundry</li>
        <li>Go on a run (important)</li>
        <li>Clean the house</li>
        <li>Go to the grocery store</li>
        <li>Do some coding</li>
        <li>Read a book (important)</li>
    </ul>

Based on this, our next step in achieving our goal is to break this view down into the
underlying data that we'd want to use to represent it. The most straightforward way to
do this would be to just put the text of each ``<li>`` into a list:

.. testcode::

    tasks = [
        "Make breakfast (important)",
        "Feed the dog (important)",
        "Do laundry",
        "Go on a run (important)",
        "Clean the house",
        "Go to the grocery store",
        "Do some coding",
        "Read a book (important)",
    ]

We could then take this list and "render" it into a series of ``<li>`` elements:

.. testcode::

    from reactpy import html

    list_item_elements = [html.li(text) for text in tasks]

This list of elements can then be passed into a parent ``<ul>`` element:

.. testcode::

    list_element = html.ul(list_item_elements)

The last thing we have to do is return this from a component:

.. reactpy:: _examples/todo_from_list


Filtering and Sorting Elements
------------------------------

Our representation of ``tasks`` worked fine to just get them on the screen, but it
doesn't extend well to the case where we want to filter and order them based on
priority. Thus, we need to change the data structure we're using to represent our tasks:

.. testcode::

    tasks = [
        {"text": "Make breakfast", "priority": 0},
        {"text": "Feed the dog", "priority": 0},
        {"text": "Do laundry", "priority": 2},
        {"text": "Go on a run", "priority": 1},
        {"text": "Clean the house", "priority": 2},
        {"text": "Go to the grocery store", "priority": 2},
        {"text": "Do some coding", "priority": 1},
        {"text": "Read a book", "priority": 1},
    ]

With this we can now imaging writing some filtering and sorting logic using Python's
:func:`filter` and :func:`sorted` functions respectively. We'll do this by only
displaying items whose ``priority`` is less than or equal to some ``filter_by_priority``
and then ordering the elements based on the ``priority``:

.. testcode::

    filter_by_priority = 1
    sort_by_priority = True

    filtered_tasks = tasks
    if filter_by_priority is not None:
        filtered_tasks = [t for t in filtered_tasks if t["priority"] <= filter_by_priority]
    if sort_by_priority:
        filtered_tasks = list(sorted(filtered_tasks, key=lambda t: t["priority"]))

    assert filtered_tasks == [
        {'text': 'Make breakfast', 'priority': 0},
        {'text': 'Feed the dog', 'priority': 0},
        {'text': 'Go on a run', 'priority': 1},
        {'text': 'Do some coding', 'priority': 1},
        {'text': 'Read a book', 'priority': 1},
    ]

We could then add this code to our ``DataList`` component:

.. warning::

    The code below produces a bunch of warnings! Be sure to tead the
    :ref:`next section <Organizing Items With Keys>` to find out why.

.. reactpy:: _examples/sorted_and_filtered_todo_list


Organizing Items With Keys
--------------------------

If you run the examples above :ref:`in debug mode <Running ReactPy in Debug Mode>` you'll
see the server log a bunch of errors that look something like:

.. code-block:: text

    Key not specified for child in list {'tagName': 'li', 'children': ...}

What this is telling us is that we haven't specified a unique ``key`` for each of the
items in our todo list. In order to silence this warning we need to expand our data
structure even further to include a unique ID for each item in our todo list:

.. testcode::

    tasks = [
        {"id": 0, "text": "Make breakfast", "priority": 0},
        {"id": 1, "text": "Feed the dog", "priority": 0},
        {"id": 2, "text": "Do laundry", "priority": 2},
        {"id": 3, "text": "Go on a run", "priority": 1},
        {"id": 4, "text": "Clean the house", "priority": 2},
        {"id": 5, "text": "Go to the grocery store", "priority": 2},
        {"id": 6, "text": "Do some coding", "priority": 1},
        {"id": 7, "text": "Read a book", "priority": 1},
    ]

Then, as we're constructing our ``<li>`` elements we'll declare a ``key`` attribute:

.. code-block::

    list_item_elements = [html.li({"key": t["id"]}, t["text"]) for t in tasks]

This ``key`` tells ReactPy which ``<li>`` element corresponds to which item of data in our
``tasks`` list. This becomes important if the order or number of items in your list can
change. In our case, if we decided to change whether we want to ``filter_by_priority``
or ``sort_by_priority`` the items in our ``<ul>`` element would change. Given this,
here's how we'd change our component:

.. reactpy:: _examples/todo_list_with_keys


Keys for Components
...................

Thus far we've been talking about passing keys to standard HTML elements. However, this
principle also applies to components too. Every function decorated with the
``@component`` decorator automatically gets a ``key`` parameter that operates in the
exact same way that it does for standard HTML elements:

.. testcode::

    from reactpy import component


    @component
    def ListItem(text):
        return html.li(text)

    tasks = [
        {"id": 0, "text": "Make breakfast"},
        {"id": 1, "text": "Feed the dog"},
        {"id": 2, "text": "Do laundry"},
        {"id": 3, "text": "Go on a run"},
        {"id": 4, "text": "Clean the house"},
        {"id": 5, "text": "Go to the grocery store"},
        {"id": 6, "text": "Do some coding"},
        {"id": 7, "text": "Read a book"},
    ]

    list_element = [ListItem(t["text"], key=t["id"]) for t in tasks]


.. warning::

    The ``key`` argument is reserved for this purpose. Defining a component with a
    function that has a ``key`` parameter will cause an error:

    .. testcode::

        from reactpy import component

        @component
        def FunctionWithKeyParam(key):
            ...

    .. testoutput::

        Traceback (most recent call last):
        ...
        TypeError: Component render function ... uses reserved parameter 'key'


Rules of Keys
.............

In order to avoid unexpected behaviors when rendering data with keys, there are a few
rules that need to be followed. These will ensure that each item of data is associated
with the correct UI element.

.. dropdown:: Keys may be the same if their elements are not siblings
    :color: info

    If two elements have different parents in the UI, they can use the same keys.

    .. testcode::

        data_1 = [
            {"id": 1, "text": "Something"},
            {"id": 2, "text": "Something else"},
        ]

        data_2 = [
            {"id": 1, "text": "Another thing"},
            {"id": 2, "text": "Yet another thing"},
        ]

        html.section(
            html.ul([html.li(data["text"], key=data["id"]) for data in data_1]),
            html.ul([html.li(data["text"], key=data["id"]) for data in data_2]),
        )

.. dropdown:: Keys must be unique amongst siblings
    :color: danger

    Keys must be unique among siblings.

    .. testcode::

        data = [
            {"id": 1, "text": "Something"},
            {"id": 2, "text": "Something else"},
            {"id": 1, "text": "Another thing"},      # BAD: has a duplicated id
            {"id": 2, "text": "Yet another thing"},  # BAD: has a duplicated id
        ]

        html.section(
            html.ul([html.li(data["text"], key=data["id"]) for data in data]),
        )

.. dropdown:: Keys must be fixed to their data.
    :color: danger

    Don't generate random values for keys to avoid the warning.

    .. testcode::

        from random import random

        data = [
            {"id": random(), "text": "Something"},
            {"id": random(), "text": "Something else"},
            {"id": random(), "text": "Another thing"},
            {"id": random(), "text": "Yet another thing"},
        ]

        html.section(
            html.ul([html.li(data["text"], key=data["id"]) for data in data]),
        )

    Doing so will result in unexpected behavior.

Since we've just been working with a small amount of sample data thus far, it was easy
enough for us to manually add an ``id`` key to each item of data. Often though, we have
to work with data that already exists. In those cases, how should we pick what value to
use for each ``key``?

- If your data comes from your database you should use the keys and IDs generated by
  that database since these are inherently unique. For example, you might use the
  primary key of records in a relational database.

- If your data is generated and persisted locally (e.g. notes in a note-taking app), use
  an incrementing counter or :mod:`uuid` from the standard library when creating items.


----------


.. card::
    :link: /guides/understanding-reactpy/why-reactpy-needs-keys
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    Learn about why ReactPy needs keys in the first place.

docs/source/guides/creating-interfaces/html-with-reactpy/index.rst METASEP
HTML With ReactPy
=================

In a typical Python-base web application the responsibility of defining the view along
with its backing data and logic are distributed between a client and server
respectively. With ReactPy, both these tasks are centralized in a single place. This is
done by allowing HTML interfaces to be constructed in Python. Take a look at the two
code examples below. The first one shows how to make a basic title and todo list using
standard HTML, the second uses ReactPy in Python, and below is a view of what the HTML
would look like if displayed:

.. grid:: 1 1 2 2
    :margin: 0
    :padding: 0

    .. grid-item::

        .. code-block:: html

            <h1>My Todo List</h1>
            <ul>
                <li>Build a cool new app</li>
                <li>Share it with the world!</li>
            </ul>

    .. grid-item::

        .. testcode::

            from reactpy import html

            html.h1("My Todo List")
            html.ul(
                html.li("Build a cool new app"),
                html.li("Share it with the world!"),
            )

    .. grid-item-card::
        :columns: 12

        .. raw:: html

            <div style="width: 50%; margin: auto;">
                <h2 style="margin-top: 0px !important;">My Todo List</h2>
                <ul>
                    <li>Build a cool new app</li>
                    <li>Share it with the world!</li>
                </ul>
            </div>

What this shows is that you can recreate the same HTML layouts with ReactPy using functions
from the :mod:`reactpy.html` module. These function share the same names as their
corresponding HTML tags. For instance, the ``<h1/>`` element above has a similarly named
:func:`~reactpy.html.h1` function. With that said, while the code above looks similar, it's
not very useful because we haven't captured the results from these function calls in a
variable. To do this we need to wrap up the layout above into a single
:func:`~reactpy.html.div` and assign it to a variable:

.. testcode::

    layout = html.div(
        html.h1("My Todo List"),
        html.ul(
            html.li("Build a cool new app"),
            html.li("Share it with the world!"),
        ),
    )


Adding HTML Attributes
----------------------

That's all well and good, but there's more to HTML than just text. What if we wanted to
display an image? In HTMl we'd use the ``<img>`` element and add attributes to it order
to specify a URL to its ``src`` and use some ``style`` to modify and position it:

.. code-block:: html

    <img
        src="https://picsum.photos/id/237/500/300"
        class="img-fluid"
        style="width: 50%; margin-left: 25%;"
        alt="Billie Holiday"
        tabindex="0"
    />

In ReactPy we add these attributes to elements using a dictionary:

.. testcode::

    html.img(
        {
            "src": "https://picsum.photos/id/237/500/300",
            "class_name": "img-fluid",
            "style": {"width": "50%", "margin_left": "25%"},
            "alt": "Billie Holiday",
        }
    )

.. raw:: html

    <!-- no tabindex since that would ruin accesibility of the page -->
    <img
        src="https://picsum.photos/id/237/500/300"
        class="img-fluid"
        style="width: 50%; margin-left: 25%;"
        alt="Billie Holiday"
    />

There are some notable differences. First, all names in ReactPy use ``snake_case`` instead
of dash-separated words. For example, ``tabindex`` and ``margin-left`` become
``tab_index`` and ``margin_left`` respectively. Second, instead of using a string to
specify the ``style`` attribute, we use a dictionary to describe the CSS properties we
want to apply to an element. This is done to avoid having to escape quotes and other
characters in the string. Finally, the ``class`` attribute is renamed to ``class_name``
to avoid conflicting with the ``class`` keyword in Python.

For full list of supported attributes and differences from HTML, see the
:ref:`HTML Attributes` reference.

----------


.. card::
    :link: /guides/understanding-reactpy/representing-html
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    Dive into the data structures ReactPy uses to represent HTML

docs/source/guides/adding-interactivity/state-as-a-snapshot/index.rst METASEP
State as a Snapshot
===================

When you watch the user interfaces you build change as you interact with them, it's easy
to imagining that they do so because there's some bit of code that modifies the relevant
parts of the view directly. As an illustration, you may think that when a user clicks a
"Send" button, there's code which reaches into the view and adds some text saying
"Message sent!":

.. image:: _static/direct-state-change.png

ReactPy works a bit differently though - user interactions cause event handlers to
:ref:`"set state" <Introduction to use_state>` triggering ReactPy to re-render a new
version of the view rather then mutating the existing one.

.. image:: _static/reactpy-state-change.png

Given this, when ReactPy "renders" something, it's as if ReactPy has taken a snapshot of the
UI where all the event handlers, local variables and the view itself were calculated
using what state was present at the time of that render. Then, when user iteractions
trigger state setters, ReactPy is made away of the newly set state and schedules a
re-render. When this subsequent renders occurs it performs all the same calculations as
before, but with this new state.

As we've :ref:`already seen <When Variables Aren't Enough>`, state variables are not
like normal variables. Instead, they live outside your components and are managed by
ReactPy. When a component is rendered, ReactPy provides the component a snapshot of the state
in that exact moment. As a result, the view returned by that component is itself a
snapshot of the UI at that time.


Setting State Triggers Renders
------------------------------

Setting state does not impact the current render, instead it schedules a re-render. It's
only in this subsequent render that changes to state take effect. As a result, setting
state more than once in the context of the same render will not cause those changes to
compound. This makes it easier to reason about how your UI will react to user
interactions because state does not change until the next render.

Let's experiment with this behaviors of state to see why we should think about it with
respect to these "snapshots" in time. Take a look at the example below and try to guess
how it will behave. **What will the count be after you click the "Increment" button?**

.. reactpy:: _examples/set_counter_3_times

Despite the fact that we called ``set_count(count + 1)`` three times, the count only
increments by ``1``! This is perhaps a surprising result, but let's break what's
happening inside the event handler to see why this is happening:

.. code-block::

    set_count(count + 1)
    set_count(count + 1)
    set_count(count + 1)

On the initial render of your ``Counter`` the ``number`` variable is ``0``. Because we
know that state variables do not change until the next render we ought to be able to
substitute ``number`` with ``0`` everywhere it's referenced within the component until
then. That includes the event handler too we should be able to rewrite the three lines
above as:

.. code-block::

    set_count(0 + 1)
    set_count(0 + 1)
    set_count(0 + 1)

Even though, we called ``set_count`` three times with what might have seemed like
different values, every time we were actually just doing ``set_count(1)`` on each call.
Only after the event handler returns will ReactPy actually perform the next render where
count is ``1``. When it does, ``number`` will be ``1`` and we'll be able to perform the
same subtitution as before to see what the next number will be after we click
"Increment":

.. code-block::

    set_count(1 + 1)
    set_count(1 + 1)
    set_count(1 + 1)


State And Delayed Reactions
---------------------------

Given what we :ref:`learned above <setting state triggers renders>`, we ought to be able
to reason about what should happen in the example below. What will be printed when the
"Increment" button is clicked?

.. reactpy:: _examples/print_count_after_set

If we use the same subtitution trick we saw before, we can rewrite these lines:

.. code-block::

    set_number(number + 5)
    print(number)

Using the value of ``number`` in the initial render which is ``0``:

.. code-block::

    set_number(0 + 5)
    print(0)

Thus when we click the button we should expect that the next render will show ``5``, but
we will ``print`` the number ``0`` instead. The next time we click the view will show
``10`` and the printout will be ``5``. In this sense the print statement, because it
lives within the prior snapshot, trails what is displayed in the next render.

What if we slightly modify this example, by introducing a delay between when we call
``set_number`` and when we print? Will this behavior remain the same? To add this delay
we'll use an :ref:`async event handler` and :func:`~asyncio.sleep` for some time:

.. reactpy:: _examples/delayed_print_after_set

Even though the render completed before the print statement took place, the behavior
remained the same! Despite the fact that the next render took place before the print
statement did, the print statement still relies on the state snapshot from the initial
render. Thus we can continue to use our substitution trick to analyze what's happening:

.. code-block::

    set_number(0 + 5)
    print("about to print...")
    await asyncio.sleep(3)
    print(0)

This property of state, that it remains static within the context of particular render,
while unintuitive at first, is actually an important tool for preventing subtle bugs.
Let's consider the example below where there's a form that sends a message with a 5
second delay. Imagine a scenario where the user:

1. Presses the "Send" button with the message "Hello" where "Alice" is the recipient.
2. Then, before the five-second delay ends, the user changes the "To" field to "Bob".

The first question to ask is "What should happen?" In this case, the user's expectation
is that after they press "Send", changing the recipient, even if the message has not
been sent yet, should not impact where the message is ultimately sent. We then need to
ask what actually happens. Will it print “You said Hello to Alice” or “You said Hello to
Bob”?

.. reactpy:: _examples/print_chat_message

As it turns out, the code above matches the user's expectation. This is because ReactPy
keeps the state values fixed within the event handlers defined during a particular
render. As a result, you don't need to worry about whether state has changed while
code in an event handler is running.

.. card::
    :link: ../multiple-state-updates/index
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    What if you wanted to read the latest state values before the next render? You’ll
    want to use a state updater function, covered on the next page!

docs/source/guides/adding-interactivity/responding-to-events/index.rst METASEP
Responding to Events
====================

ReactPy lets you add event handlers to your parts of the interface. These events handlers
are functions which can be assigned to a part of a UI such that, when a user iteracts
with the interface, those functions get triggered. Examples of interaction include
clicking, hovering, of focusing on form inputs, and more.


Adding Event Handlers
---------------------

To start out we'll just display a button that, for the moment, doesn't do anything:

.. reactpy:: _examples/button_does_nothing

To add an event handler to this button we'll do three things:

1. Declare a function called ``handle_event(event)`` inside the body of our ``Button`` component
2. Add logic to ``handle_event`` that will print the ``event`` it receives to the console.
3. Add an ``"onClick": handle_event`` attribute to the ``<button>`` element.

.. reactpy:: _examples/button_prints_event

.. note::

    Normally print statements will only be displayed in the terminal where you launched
    ReactPy.

It may feel weird to define a function within a function like this, but doing so allows
the ``handle_event`` function to access information from within the scope of the
component. That's important if you want to use any arguments that may have beend passed
your component in the handler:

.. reactpy:: _examples/button_prints_message

With all that said, since our ``handle_event`` function isn't doing that much work, if
we wanted to streamline our component definition, we could pass in our event handler as a
lambda:

.. code-block::

    html.button({"onClick": lambda event: print(message_text)}, "Click me!")


Supported Event Types
---------------------

Since ReactPy's event information comes from React, most the the information (:ref:`with
some exceptions <event data Serialization>`) about how React handles events translates
directly to ReactPy. Follow the links below to learn about each category of event:

- :ref:`Clipboard Events`
- :ref:`Composition Events`
- :ref:`Keyboard Events`
- :ref:`Focus Events`
- :ref:`Form Events`
- :ref:`Generic Events`
- :ref:`Mouse Events`
- :ref:`Pointer Events`
- :ref:`Selection Events`
- :ref:`Touch Events`
- :ref:`UI Events`
- :ref:`Wheel Events`
- :ref:`Media Events`
- :ref:`Image Events`
- :ref:`Animation Events`
- :ref:`Transition Events`
- :ref:`Other Events`


Passing Handlers to Components
------------------------------

A common pattern when factoring out common logic is to pass event handlers into a more
generic component definition. This allows the component to focus on the things which are
common while still giving its usages customizablity. Consider the case below where we
want to create a generic ``Button`` component that can be used for a variety of purpose:

.. reactpy:: _examples/button_handler_as_arg


.. _Async Event Handler:

Async Event Handlers
--------------------

Sometimes event handlers need to execute asynchronous tasks when they are triggered.
Behind the scenes, ReactPy is running an :mod:`asyncio` event loop for just this purpose.
By defining your event handler as an asynchronous function instead of a normal
synchronous one. In the layout below we sleep for several seconds before printing out a
message in the first button. However, because the event handler is asynchronous, the
handler for the second button is still able to respond:

.. reactpy:: _examples/button_async_handlers


Event Data Serialization
------------------------

Not all event data is serialized. The most notable example of this is the lack of a
``target`` key in the dictionary sent back to the handler. Instead, data which is not
inherently JSON serializable must be treated on a case-by-case basis. A simple case
to demonstrate this is the ``currentTime`` attribute of ``audio`` and ``video``
elements. Normally this would be accessible via ``event.target.currentTime``, but here
it's simply passed in under the key ``currentTime``:

.. reactpy:: _examples/audio_player


Client-side Event Behavior
--------------------------

Because ReactPy operates server-side, there are inevitable limitations that prevent it from
achieving perfect parity with all the behaviors of React. With that said, any feature
that cannot be achieved in Python with ReactPy, can be done by creating
:ref:`Custom Javascript Components`.


Preventing Default Event Actions
................................

Instead of calling an ``event.preventDefault()`` method as you would do in React, you
must declare whether to prevent default behavior ahead of time. This can be accomplished
using the :func:`~reactpy.core.events.event` decorator and setting ``prevent_default``. For
example, we can stop a link from going to the specified URL:

.. reactpy:: _examples/prevent_default_event_actions

Unfortunately this means you cannot conditionally prevent default behavior in response
to event data without writing :ref:`Custom Javascript Components`.


Stop Event Propogation
......................

Similarly to :ref:`preventing default behavior <Preventing Default Event Actions>`, you
can use the :func:`~reactpy.core.events.event` decorator to prevent events originating in a
child element from propagating to parent elements by setting ``stop_propagation``. In
the example below we place a red ``div`` inside a parent blue ``div``. When propogation
is turned on, clicking the red element will cause the handler for the outer blue one to
trigger. Conversely, when it's off, only the handler for the red element will trigger.

.. reactpy:: _examples/stop_event_propagation

docs/source/guides/adding-interactivity/multiple-state-updates/index.rst METASEP
Multiple State Updates
======================

Setting a state variable will queue another render. But sometimes you might want to
perform multiple operations on the value before queueing the next render. To do this, it
helps to understand how React batches state updates.


Batched Updates
---------------

As we learned :ref:`previously <state as a snapshot>`, state variables remain fixed
inside each render as if state were a snapshot taken at the begining of each render.
This is why, in the example below, even though it might seem like clicking the
"Increment" button would cause the ``number`` to increase by ``3``, it only does by
``1``:

.. reactpy:: ../state-as-a-snapshot/_examples/set_counter_3_times

The reason this happens is because, so long as the event handler is synchronous (i.e.
the event handler is not an ``async`` function), ReactPy waits until all the code in an
event handler has run before processing state and starting the next render. Thus, it's
the last call to a given state setter that matters. In the example below, even though we
set the color of the button to ``"orange"`` and then ``"pink"`` before ``"blue"``,
the color does not quickly flash orange and pink before blue - it alway remains blue:

.. reactpy:: _examples/set_color_3_times

This behavior let's you make multiple state changes without triggering unnecessary
renders or renders with inconsistent state where only some of the variables have been
updated. With that said, it also means that the UI won't change until after synchronous
handlers have finished running.

.. note::

    For asynchronous event handlers, ReactPy will not render until you ``await`` something.
    As we saw in :ref:`prior examples <State And Delayed Reactions>`, if you introduce
    an asynchronous delay to an event handler after changing state, renders may take
    place before the remainder of the event handler completes. However, state variables
    within handlers, even async ones, always remains static.

This behavior of ReactPy to "batch" state changes that take place inside a single event
handler, do not extend across event handlers. In other words, distinct events will
always produce distinct renders. To give an example, if clicking a button increments a
counter by one, no matter how fast the user clicks, the view will never jump from 1 to 3
- it will always display 1, then 2, and then 3.


Incremental Updates
-------------------

While it's uncommon, you need to update a state variable more than once before the next
render. In these cases, instead of having updates batched, you instead want them to be
applied incrementally. That is, the next update can be made to depend on the prior one.
For example, what it we wanted to make it so that, in our ``Counter`` example :ref:`from
before <Batched Updates>`, each call to ``set_number`` did in fact increment
``number`` by one causing the view to display ``0``, then ``3``, then ``6``, and so on?

To accomplish this, instead of passing the next state value as in ``set_number(number +
1)``, we may pass an **"updater function"** to ``set_number`` that computes the next
state based on the previous state. This would look like ``set_number(lambda number:
number + 1)``. In other words we need a function of the form:

.. code-block::

    def compute_new_state(old_state):
        ...
        return new_state

In our case, ``new_state = old_state + 1``. So we might define:

.. code-block::

    def increment(old_number):
        new_number = old_number + 1
        return new_number

Which we can use to replace ``set_number(number + 1)`` with ``set_number(increment)``:

.. reactpy:: _examples/set_state_function

The way to think about how ReactPy runs though this series of ``set_state(increment)``
calls is to imagine that each one updates the internally managed state with its return
value, then that return value is being passed to the next updater function. Ultimately,
this is functionally equivalent to the following:

.. code-block::

    set_number(increment(increment(increment(number))))

So why might you want to do this? Why not just compute ``set_number(number + 3)`` from
the start? The easiest way to explain the use case is with an example. Imagine that we
introduced a delay before ``set_number(number + 1)``. What would happen if we clicked
the "Increment" button more than once before the delay in the first triggered event
completed?

.. reactpy:: _examples/delay_before_set_count

From an :ref:`earlier lesson <State And Delayed Reactions>`, we learned that introducing
delays do not change the fact that state variables do not change until the next render.
As a result, despite clicking many times before the delay completes, the ``number`` only
increments by one. To solve this we can use updater functions:

.. reactpy:: _examples/delay_before_count_updater

Now when you click the "Increment" button, each click, though delayed, corresponds to
``number`` being increased. This is because the ``old_number`` in the updater function
uses the value which was assigned by the last call to ``set_number`` rather than relying
in the static ``number`` state variable.

docs/source/guides/adding-interactivity/dangers-of-mutability/index.rst METASEP
Dangers of Mutability
=====================

While state can hold any type of value, you should be careful to avoid directly
modifying objects that you declare as state with ReactPy. In other words, you must not
:ref:`"mutate" <What is a Mutation>` values which are held as state. Rather, to change
these values you should use new ones or create copies.


.. _what is a mutation:

What is a Mutation?
-------------------

In Python, values may be either "mutable" or "immutable". Mutable objects are those
whose underlying data can be changed after they are created, and immutable objects are
those which cannot. A "mutation" then, is the act of changing the underlying data of a
mutable value. In particular, a :class:`dict` is a mutable type of value. In the code
below, an initially empty dictionary is created. Then, a key and value is added to it:

.. code-block::

    x = {}
    x["a"] = 1
    assert x == {"a": 1}

This is different from something like a :class:`str` which is immutable. Instead of
modifying the underlying data of an existing value, a new one must be created to
facilitate change:

.. code-block::

    x = "Hello"
    y = x + " world!"
    assert x is not y

.. note::

    In Python, the ``is`` and ``is not`` operators check whether two values are
    identitcal. This `is distinct
    <https://realpython.com/python-is-identity-vs-equality>`__ from checking whether two
    values are equivalent with the ``==`` or ``!=`` operators.

Thus far, all the values we've been working with have been immutable. These include
:class:`int`, :class:`float`, :class:`str`, and :class:`bool` values. As a result, we've
have not had to consider the consiquences of mutations.


.. _Why Avoid Mutation:

Why Avoid Mutation?
-------------------

Unfortunately, ReactPy does not understand that when a value is mutated, it may have
changed. As a result, mutating values will not trigger re-renders. Thus, you must be
careful to avoid mutation whenever you want ReactPy to re-render a component. For example,
the intention of the code below is to make the red dot move when you touch or hover over
the preview area. However it doesn't - the dot remains stationary:

.. reactpy:: _examples/moving_dot_broken

The problem is with this section of code:

.. literalinclude:: _examples/moving_dot_broken.py
    :language: python
    :lines: 13-14
    :linenos:
    :lineno-start: 13

This code mutates the ``position`` dictionary from the prior render instead of using the
state variable's associated state setter. Without calling setter ReactPy has no idea that
the variable's data has been modified. While it can be possible to get away with
mutating state variables, it's highly dicsouraged. Doing so can cause strange and
unpredictable behavior. As a result, you should always treat the data within a state
variable as immutable.

To actually trigger a render we need to call the state setter. To do that we'll assign
it to ``set_position`` instead of the unused ``_`` variable we have above. Then we can
call it by passing a *new* dictionary with the values for the next render. Notice how,
by making these alterations to the code, that the dot now follows your pointer when
you touch or hover over the preview:

.. reactpy:: _examples/moving_dot


.. dropdown:: Local mutation can be alright
    :color: info
    :animate: fade-in

    While code like this causes problems:

    .. code-block::

        position["x"] = event["clientX"] - outer_div_bounds["x"]
        position["y"] = event["clientY"] - outer_div_bounds["y"]

    It's ok if you mutate a fresh dictionary that you have *just* created before calling
    the state setter:

    .. code-block::

        new_position = {}
        new_position["x"] = event["clientX"] - outer_div_bounds["x"]
        new_position["y"] = event["clientY"] - outer_div_bounds["y"]
        set_position(new_position)

    It's actually nearly equivalent to having written:

    .. code-block::

        set_position(
            {
                "x": event["clientX"] - outer_div_bounds["x"],
                "y": event["clientY"] - outer_div_bounds["y"],
            }
        )

    Mutation is only a problem when you change data assigned to existing state
    variables. Mutating an object you’ve just created is okay because no other code
    references it yet. Changing it isn’t going to accidentally impact something that
    depends on it. This is called a “local mutation.” You can even do local mutation
    while rendering. Very convenient and completely okay!


Working with Dictionaries
-------------------------

Below are some ways to update dictionaries without mutating them:

.. card:: Updating Items
    :link: updating-dictionary-items
    :link-type: ref

    Avoid using item assignment, ``dict.update``, or ``dict.setdefault``. Instead try
    the strategies below:

    .. code-block::

        {**d, "key": value}

        # Python >= 3.9
        d | {"key": value}

        # Equivalent to dict.setdefault()
        {"key": value, **d}

.. card:: Removing Items
    :link: removing-dictionary-items
    :link-type: ref

    Avoid using item deletion or ``dict.pop``. Instead try the strategies below:

    .. code-block::

        {
            k: v
            for k, v in d.items()
            if k != key
        }

        # Better for removing multiple items
        {
            k: d[k]
            for k in set(d).difference([key])
        }


----


.. _updating-dictionary-items:

Updating Dictionary Items
.........................

.. grid:: 1 1 1 2
    :gutter: 1

    .. grid-item-card:: :bdg-danger:`Avoid`

        .. code-block::

            d[key] = value

            d.update({key: value})

            d.setdefault(key, value)

    .. grid-item-card:: :bdg-info:`Prefer`

        .. code-block::

            {**d, key: value}

            # Python >= 3.9
            d | {key: value}

            # Equivalent to setdefault()
            {key: value, **d}

As we saw in an :ref:`earlier example <why avoid mutation>`, instead of mutating
dictionaries to update their items you should instead create a copy that contains the
desired changes.

However, sometimes you may only want to update some of the information in a dictionary
which is held by a state variable. Consider the case below where we have a form for
updating user information with a preview of the currently entered data. We can
accomplish this using `"unpacking" <https://www.python.org/dev/peps/pep-0448/>`__ with
the ``**`` syntax:

.. reactpy:: _examples/dict_update


.. _removing-dictionary-items:

Removing Dictionary Items
.........................

.. grid:: 1 1 1 2
    :gutter: 1

    .. grid-item-card:: :bdg-danger:`Avoid`

        .. code-block::

            del d[key]

            d.pop(key)

    .. grid-item-card:: :bdg-info:`Prefer`

        .. code-block::

            {
                k: v
                for k, v in d.items()
                if k != key
            }

            # Better for removing multiple items
            {
                k: d[k]
                for k in set(d).difference([key])
            }

This scenario doesn't come up very frequently. When it does though, the best way to
remove items from dictionaries is to create a copy of the original, but with a filtered
set of keys. One way to do this is with a dictionary comprehension. The example below
shows an interface where you're able to enter a new term and definition. Once added,
you can click a delete button to remove the term and definition:

.. reactpy:: _examples/dict_remove


Working with Lists
------------------

Below are some ways to update lists without mutating them:

.. card:: Inserting Items
    :link: inserting-list-items
    :link-type: ref

    Avoid using ``list.append``, ``list.extend``, and ``list.insert``. Instead try the
    strategies below:

    .. code-block::

        [*l, value]

        l + [value]

        l + values

        l[:index] + [value] + l[index:]

.. card:: Removing Items
    :link: removing-list-items
    :link-type: ref

    Avoid using item deletion or ``list.pop``. Instead try the strategy below:

    .. code-block::

        l[:index - 1] + l[index:]

.. card:: Replacing Items
    :link: replacing-list-items
    :link-type: ref

    Avoid using item  or slice assignment. Instead try the strategies below:

    .. code-block::

        l[:index] + [value] + l[index + 1:]

        l[:start] + values + l[end + 1:]

.. card:: Re-ordering Items
    :link: re-ordering-list-items
    :link-type: ref

    Avoid using ``list.sort`` or ``list.reverse``. Instead try the strategies below:

    .. code-block::

        list(sorted(l))

        list(reversed(l))


----


.. _inserting-list-items:

Inserting List Items
....................

.. grid:: 1 1 1 2

    .. grid-item-card:: :bdg-danger:`Avoid`

        .. code-block::

            l.append(value)

            l.extend(values)

            l.insert(index, value)

            # Adding a list "in-place" mutates!
            l += [value]

    .. grid-item-card:: :bdg-info:`Prefer`

        .. code-block::

            [*l, value]

            l + [value]

            l + values

            l[:index] + [value] + l[index:]

Instead of mutating a list to add items to it, we need to create a new list which has
the items we want to append instead. There are several ways to do this for one or more
values however it's often simplest to use `"unpacking"
<https://www.python.org/dev/peps/pep-0448/>`__ with the ``*`` syntax.

.. reactpy:: _examples/list_insert


.. _removing-list-items:

Removing List Items
...................

.. grid:: 1 1 1 2

    .. grid-item-card:: :bdg-danger:`Avoid`

        .. code-block::

            del l[index]

            l.pop(index)

    .. grid-item-card:: :bdg-info:`Prefer`

        .. code-block::

            l[:index] + l[index + 1:]

Unfortunately, the syntax for creating a copy of a list with one of its items removed is
not quite as clean. You must select the portion the list prior to the item which should
be removed (``l[:index]``) and the portion after the item (``l[index + 1:]``) and add
them together:

.. reactpy:: _examples/list_remove


.. _replacing-list-items:

Replacing List Items
....................

.. grid:: 1 1 1 2

    .. grid-item-card:: :bdg-danger:`Avoid`

        .. code-block::

            l[index] = value

            l[start:end] = values

    .. grid-item-card:: :bdg-info:`Prefer`

        .. code-block::

            l[:index] + [value] + l[index + 1:]

            l[:start] + values + l[end + 1:]

In a similar manner to :ref:`removing list items`, to replace an item in a list, you
must select the portion before and after the item in question. But this time, instead
of adding those two selections together, you must insert that values you want to replace
between them:

.. reactpy:: _examples/list_replace


.. _re-ordering-list-items:

Re-ordering List Items
......................

.. grid:: 1 1 1 2

    .. grid-item-card:: :bdg-danger:`Avoid`

        .. code-block::

            l.sort()

            l.reverse()

    .. grid-item-card:: :bdg-info:`Prefer`

        .. code-block::

            list(sorted(l))

            list(reversed(l))

There are many different ways that list items could be re-ordered, but two of the most
common are reversing or sorting items. Instead of calling the associated methods on a
list object, you should use the builtin functions :func:`sorted` and :func:`reversed`
and pass the resulting iterator into the :class:`list` constructor to create a sorted
or reversed copy of the given list:

.. reactpy:: _examples/list_re_order


Working with Sets
-----------------

Below are ways to update sets without mutating them:

.. card:: Adding Items
    :link: adding-set-items
    :link-type: ref

    Avoid using item assignment, ``set.add`` or ``set.update``. Instead try the
    strategies below:

    .. code-block::

        s.union({value})

        s.union(values)

.. card:: Removing Items
    :link: removing-set-items
    :link-type: ref

    Avoid using item deletion or ``dict.pop``. Instead try the strategies below:

    .. code-block::

        s.difference({value})

        s.difference(values)

        s.intersection(values)


----


.. _adding-set-items:

Adding Set Items
................

.. grid:: 1 1 1 2

    .. grid-item-card:: :bdg-danger:`Avoid`

        .. code-block::

            s.add(value)
            s |= {value}  # "in-place" operators mutate!

            s.update(values)
            s |= values  # "in-place" operators mutate!

    .. grid-item-card:: :bdg-info:`Prefer`

        .. code-block::

            s.union({value})
            s | {value}

            s.union(values)
            s | values

Sets have some nice ways for evolving them without requiring mutation. The binary
or operator ``|`` serves as a succinct way to compute the union of two sets. However,
you should be careful to not use an in-place assignment with this operator as that will
(counterintuitively) mutate the original set rather than creating a new one.

.. reactpy:: _examples/set_update


.. _removing-set-items:

Removing Set Items
..................

.. grid:: 1 1 1 2

    .. grid-item-card:: :bdg-danger:`Avoid`

        .. code-block::

            s.remove(value)

            s.difference_update(values)
            s -= values  # "in-place" operators mutate!

            s.symetric_difference_update(values)
            s ^= values  # "in-place" operators mutate!

            s.intersection_update(values)
            s &= values  # "in-place" operators mutate!


    .. grid-item-card:: :bdg-info:`Prefer`

        .. code-block::

            s.difference({value})

            s.difference(values)
            s - values

            s.symetric_difference(values)
            s ^ values

            s.intersection(values)
            s & values

To remove items from sets you can use the various binary operators or their associated
methods to return new sets without mutating them. As before when :ref:`adding set items`
you need to avoid using the inline assignment operators since that will
(counterintuitively) mutate the original set rather than given you a new one:

.. reactpy:: _examples/set_remove


Useful Packages
---------------

Under construction 🚧

https://pypi.org/project/pyrsistent/

docs/source/guides/adding-interactivity/components-with-state/index.rst METASEP
Components With State
=====================

Components often need to change what’s on the screen as a result of an interaction. For
example, typing into the form should update the input field, clicking “next” on an image
carousel should change which image is displayed, clicking “buy” should put a product in
the shopping cart. Components need to “remember” things like the current input value,
the current image, the shopping cart. In ReactPy, this kind of component-specific memory is
called state.


When Variables Aren't Enough
----------------------------

Below is a gallery of images about sculpture. Clicking the "Next" button should
increment the ``index`` and, as a result, change what image is displayed. However, this
does not work:

.. reactpy:: _examples/when_variables_are_not_enough

.. note::

    Try clicking the button to see that it does not cause a change.

After clicking "Next", if you check your web server's logs, you'll discover an
``UnboundLocalError`` error. It turns out that in this case, the ``index = index + 1``
statement is similar to `trying to set global variables
<https://stackoverflow.com/questions/9264763/dont-understand-why-unboundlocalerror-occurs-closure>`__.
Technically there's a way to `fix this error
<https://docs.python.org/3/reference/simple_stmts.html#nonlocal>`__, but even if we did,
that still wouldn't fix the underlying problems:

1. **Local variables do not persist across component renders** - when a component is
   updated, its associated function gets called again. That is, it renders. As a result,
   all the local state that was created the last time the function was called gets
   destroyed when it updates.

2. **Changes to local variables do not cause components to re-render** - there's no way
   for ReactPy to observe when these variables change. Thus ReactPy is not aware that
   something has changed and that a re-render should take place.

To address these problems, ReactPy provides the :func:`~reactpy.core.hooks.use_state` "hook"
which provides:

1. A **state variable** whose data is retained across renders.

2. A **state setter** function that can be used to update that variable and trigger a
   render.


Adding State to Components
--------------------------

To create a state variable and state setter with :func:`~reactpy.core.hooks.use_state` hook
as described above, we'll begin by importing it:

.. testcode::

    from reactpy import use_state

Then we'll make the following changes to our code :ref:`from before <When Variables
Aren't Enough>`:

.. code-block:: diff

    -    index = 0
    +    index, set_index = use_state

         def handle_click(event):
    -        index = index + 1
    +        set_index(index + 1)

After making those changes we should get:

.. code-block::
    :linenos:
    :lineno-start: 14

        index, set_index = use_state(0)

        def handle_click(event):
            set_index(index + 1)

We'll talk more about what this is doing :ref:`shortly <your first hook>`, but for
now let's just verify that this does in fact fix the problems from before:

.. reactpy:: _examples/adding_state_variable


Your First Hook
---------------

In ReactPy, ``use_state``, as well as any other function whose name starts with ``use``, is
called a "hook". These are special functions that should only be called while ReactPy is
:ref:`rendering <the rendering process>`. They let you "hook into" the different
capabilities of ReactPy's components of which ``use_state`` is just one (well get into the
other :ref:`later <managing state>`).

While hooks are just normal functions, but it's helpful to think of them as
:ref:`unconditioned <rules of hooks>` declarations about a component's needs. In other
words, you'll "use" hooks at the top of your component in the same way you might
"import" modules at the top of your Python files.


.. _Introduction to use_state:

Introduction to ``use_state``
-----------------------------

When you call :func:`~reactpy.core.hooks.use_state` inside the body of a component's render
function, you're declaring that this component needs to remember something. That
"something" which needs to be remembered, is known as **state**. So when we look at an
assignment expression like the one below

.. code-block::

    index, set_index = use_state(0)

we should read it as saying that ``index`` is a piece of state which must be
remembered by the component that declared it. The argument to ``use_state`` (in this
case ``0``) is then conveying what the initial value for ``index`` is.

We should then understand that each time the component which owns this state renders
``use_state`` will return a tuple containing two values - the current value of the state
(``index``) and a function to change that value the next time the component is rendered.
Thus, in this example:

- ``index`` - is a **state variable** containing the currently stored value.
- ``set_index`` - is a **state setter** for changing that value and triggering a re-render
  of the component.

The convention is that, if you name your state variable ``thing``, your state setter
should be named ``set_thing``. While you could name them anything you want, adhering to
the convention makes things easier to understand across projects.

----

To understand how this works in context, let's break down our example by examining key
moments in the execution of the ``Gallery`` component. Each numbered tab in the section
below highlights a line of code where something of interest occurs:

.. hint::

    Try clicking through the numbered tabs to each highlighted step of execution

.. tab-set::

    .. tab-item:: 1

        .. raw:: html

            <h2>Initial render</h2>

        .. literalinclude:: _examples/adding_state_variable/main.py
            :lines: 12-33
            :emphasize-lines: 2

        At this point, we've just begun to render the ``Gallery`` component. As yet,
        ReactPy is not aware that this component has any state or what view it will
        display. This will change in a moment though when we move to the next line...

    .. tab-item:: 2

        .. raw:: html

            <h2>Initial state declaration</h2>

        .. literalinclude:: _examples/adding_state_variable/main.py
            :lines: 12-33
            :emphasize-lines: 3

        The ``Gallery`` component has just declared some state. ReactPy now knows that it
        must remember the ``index`` and trigger an update of this component when
        ``set_index`` is called. Currently the value of ``index`` is ``0`` as per the
        default value given to ``use_state``. Thus, the resulting view will display
        information about the first item in our ``sculpture_data`` list.

    .. tab-item:: 3

        .. raw:: html

            <h2>Define event handler</h2>

        .. literalinclude:: _examples/adding_state_variable/main.py
            :lines: 12-33
            :emphasize-lines: 5

        We've now defined an event handler that we intend to assign to a button in the
        view. This will respond once the user clicks that button. The action this
        handler performs is to update the value of ``index`` and schedule our ``Gallery``
        component to update.

    .. tab-item:: 4

        .. raw:: html

            <h2>Return the view</h2>

        .. literalinclude:: _examples/adding_state_variable/main.py
            :lines: 12-33
            :emphasize-lines: 16

        The ``handle_click`` function we defined above has now been assigned to a button
        in the view and we are about to display information about the first item in out
        ``sculpture_data`` list. When the view is ultimately displayed, if a user clicks
        the "Next" button, the handler we just assigned will be triggered. Until that
        point though, the application will remain static.

    .. tab-item:: 5

        .. raw:: html

            <h2>User interaction</h2>

        .. literalinclude:: _examples/adding_state_variable/main.py
            :lines: 12-33
            :emphasize-lines: 5

        A user has just clicked the button 🖱️! ReactPy has sent information about the event
        to the ``handle_click`` function and it is about to execute. In a moment we will
        update the state of this component and schedule a re-render.

    .. tab-item:: 6

        .. raw:: html

            <h2>New state is set</h2>

        .. literalinclude:: _examples/adding_state_variable/main.py
            :lines: 12-33
            :emphasize-lines: 6

        We've just now told ReactPy that we want to update the state of our ``Gallery`` and
        that it needs to be re-rendered. More specifically, we are incrementing its
        ``index``, and once ``Gallery`` re-renders the index *will* be ``1``.
        Importantly, at this point, the value of ``index`` is still ``0``! This will
        only change once the component begins to re-render.

    .. tab-item:: 7

        .. raw:: html

            <h2>Next render begins</h2>

        .. literalinclude:: _examples/adding_state_variable/main.py
            :lines: 12-33
            :emphasize-lines: 2

        The scheduled re-render of ``Gallery`` has just begun. ReactPy has now updated its
        internal state store such that, the next time we call ``use_state`` we will get
        back the updated value of ``index``.

    .. tab-item:: 8

        .. raw:: html

            <h2>Next state is acquired</h2>

        .. literalinclude:: _examples/adding_state_variable/main.py
            :lines: 12-33
            :emphasize-lines: 3

        With ReactPy's state store updated, as we call ``use_state``, instead of returning
        ``0`` for the value of ``index`` as it did before, ReactPy now returns the value
        ``1``. With this change the view we display will be altered - instead of
        displaying data for the first item in our ``sculpture_data`` list we will now
        display information about the second.

    .. tab-item:: 9

        .. raw:: html

            <h2>Repeat...</h2>

        .. literalinclude:: _examples/adding_state_variable/main.py
            :lines: 12-33

        From this point on, the steps remain the same. The only difference being the
        progressively incrementing ``index`` each time the user clicks the "Next" button
        and the view which is altered to to reflect the currently indexed item in the
        ``sculpture_data`` list.

        .. note::

            Once we reach the end of the ``sculpture_data`` list the view will return
            back to the first item since we create a ``bounded_index`` by doing a modulo
            of the index with the length of the list (``index % len(sculpture_data)``).
            Ideally we would do this bounding at the time we call ``set_index`` to
            prevent ``index`` from incrementing to infinity, but to keep things simple
            in this examples, we've kept this logic separate.


Multiple State Declarations
---------------------------

The powerful thing about hooks like :func:`~reactpy.core.hooks.use_state` is that you're
not limited to just one state declaration. You can call ``use_state()`` as many times as
you need to in one component. For example, in the example below we've added a
``show_more`` state variable along with a few other modifications (e.g. renaming
``handle_click``) to make the description for each sculpture optionally displayed. Only
when the user clicks the "Show details" button is this description shown:

.. reactpy:: _examples/multiple_state_variables

It's generally a good idea to define separate state variables if the data they represent
is unrelated. In this case, ``index`` corresponds to what sculpture information is being
displayed and ``show_more`` is solely concerned with whether the description for a given
sculpture is shown. Put other way ``index`` is concerned with *what* information is
displayed while ``show_more`` is concerned with *how* it is displayed. Conversely
though, if you have a form with many fields, it probably makes sense to have a single
object that holds the data for all the fields rather than an object per-field.

.. note::

    This topic is discussed more in the :ref:`structuring your state` section.


State is Isolated and Private
-----------------------------

State is local to a component instance on the screen. In other words, if you render the
same component twice, each copy will have completely isolated state! Changing one of
them will not affect the other.

In this example, the ``Gallery`` component from earlier is rendered twice with no
changes to its logic. Try clicking the buttons inside each of the galleries. Notice that
their state is independent:

.. reactpy:: _examples/isolated_state
        :result-is-default-tab:

This is what makes state different from regular variables that you might declare at the
top of your module. State is not tied to a particular function call or a place in the
code, but it’s “local” to the specific place on the screen. You rendered two ``Gallery``
components, so their state is stored separately.

Also notice how the Page component doesn’t “know” anything about the Gallery state or
even whether it has any. Unlike props, state is fully private to the component declaring
it. The parent component can’t change it. This lets you add state to any component or
remove it without impacting the rest of the components.

.. card::
    :link: /guides/managing-state/sharing-component-state/index
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    What if you wanted both galleries to keep their states in sync? The right way to do
    it in ReactPy is to remove state from child components and add it to their closest
    shared parent.

docs/source/reference/_examples/character_movement/main.py METASEP
from pathlib import Path
from typing import NamedTuple

from reactpy import component, html, run, use_state
from reactpy.widgets import image


HERE = Path(__file__)
CHARACTER_IMAGE = (HERE.parent / "static" / "bunny.png").read_bytes()


class Position(NamedTuple):
    x: int
    y: int
    angle: int


def rotate(degrees):
    return lambda old_position: Position(
        old_position.x,
        old_position.y,
        old_position.angle + degrees,
    )


def translate(x=0, y=0):
    return lambda old_position: Position(
        old_position.x + x,
        old_position.y + y,
        old_position.angle,
    )


@component
def Scene():
    position, set_position = use_state(Position(100, 100, 0))

    return html.div(
        {"style": {"width": "225px"}},
        html.div(
            {
                "style": {
                    "width": "200px",
                    "height": "200px",
                    "background_color": "slategray",
                }
            },
            image(
                "png",
                CHARACTER_IMAGE,
                {
                    "style": {
                        "position": "relative",
                        "left": f"{position.x}px",
                        "top": f"{position.y}.px",
                        "transform": f"rotate({position.angle}deg) scale(2, 2)",
                    }
                },
            ),
        ),
        html.button(
            {"on_click": lambda e: set_position(translate(x=-10))}, "Move Left"
        ),
        html.button(
            {"on_click": lambda e: set_position(translate(x=10))}, "Move Right"
        ),
        html.button({"on_click": lambda e: set_position(translate(y=-10))}, "Move Up"),
        html.button({"on_click": lambda e: set_position(translate(y=10))}, "Move Down"),
        html.button({"on_click": lambda e: set_position(rotate(-30))}, "Rotate Left"),
        html.button({"on_click": lambda e: set_position(rotate(30))}, "Rotate Right"),
    )


run(Scene)

docs/source/guides/getting-started/_examples/sample_app.py METASEP
import reactpy


reactpy.run(reactpy.sample.SampleApp)

docs/source/guides/getting-started/_examples/run_tornado.py METASEP
# :lines: 11-

from reactpy import run
from reactpy.backend import tornado as tornado_server


# the run() function is the entry point for examples
tornado_server.configure = lambda _, cmpt: run(cmpt)


import tornado.ioloop
import tornado.web

from reactpy import component, html
from reactpy.backend.tornado import configure


@component
def HelloWorld():
    return html.h1("Hello, world!")


def make_app():
    app = tornado.web.Application()
    configure(app, HelloWorld)
    return app


if __name__ == "__main__":
    app = make_app()
    app.listen(8000)
    tornado.ioloop.IOLoop.current().start()

docs/source/guides/getting-started/_examples/run_starlette.py METASEP
# :lines: 11-

from reactpy import run
from reactpy.backend import starlette as starlette_server


# the run() function is the entry point for examples
starlette_server.configure = lambda _, cmpt: run(cmpt)


from starlette.applications import Starlette

from reactpy import component, html
from reactpy.backend.starlette import configure


@component
def HelloWorld():
    return html.h1("Hello, world!")


app = Starlette()
configure(app, HelloWorld)

docs/source/guides/getting-started/_examples/run_sanic.py METASEP
# :lines: 11-

from reactpy import run
from reactpy.backend import sanic as sanic_server


# the run() function is the entry point for examples
sanic_server.configure = lambda _, cmpt: run(cmpt)


from sanic import Sanic

from reactpy import component, html
from reactpy.backend.sanic import configure


@component
def HelloWorld():
    return html.h1("Hello, world!")


app = Sanic("MyApp")
configure(app, HelloWorld)


if __name__ == "__main__":
    app.run(port=8000)

docs/source/guides/getting-started/_examples/run_flask.py METASEP
# :lines: 11-

from reactpy import run
from reactpy.backend import flask as flask_server


# the run() function is the entry point for examples
flask_server.configure = lambda _, cmpt: run(cmpt)


from flask import Flask

from reactpy import component, html
from reactpy.backend.flask import configure


@component
def HelloWorld():
    return html.h1("Hello, world!")


app = Flask(__name__)
configure(app, HelloWorld)

docs/source/guides/getting-started/_examples/run_fastapi.py METASEP
# :lines: 11-

from reactpy import run
from reactpy.backend import fastapi as fastapi_server


# the run() function is the entry point for examples
fastapi_server.configure = lambda _, cmpt: run(cmpt)


from fastapi import FastAPI

from reactpy import component, html
from reactpy.backend.fastapi import configure


@component
def HelloWorld():
    return html.h1("Hello, world!")


app = FastAPI()
configure(app, HelloWorld)

docs/source/guides/getting-started/_examples/hello_world.py METASEP
from reactpy import component, html, run


@component
def App():
    return html.h1("Hello, world!")


run(App)

docs/source/guides/getting-started/_examples/debug_error_example.py METASEP
from reactpy import component, html, run


@component
def App():
    return html.div(GoodComponent(), BadComponent())


@component
def GoodComponent():
    return html.p("This component rendered successfuly")


@component
def BadComponent():
    raise RuntimeError("This component raised an error")


run(App)

docs/source/guides/escape-hatches/_examples/material_ui_button_on_click.py METASEP
import json

import reactpy


mui = reactpy.web.module_from_template(
    "react@^17.0.0",
    "@material-ui/core@4.12.4",
    fallback="⌛",
)
Button = reactpy.web.export(mui, "Button")


@reactpy.component
def ViewButtonEvents():
    event, set_event = reactpy.hooks.use_state(None)

    return reactpy.html.div(
        Button(
            {
                "color": "primary",
                "variant": "contained",
                "onClick": lambda event: set_event(event),
            },
            "Click Me!",
        ),
        reactpy.html.pre(json.dumps(event, indent=2)),
    )


reactpy.run(ViewButtonEvents)

docs/source/guides/escape-hatches/_examples/material_ui_button_no_action.py METASEP
from reactpy import component, run, web


mui = web.module_from_template(
    "react@^17.0.0",
    "@material-ui/core@4.12.4",
    fallback="⌛",
)
Button = web.export(mui, "Button")


@component
def HelloWorld():
    return Button({"color": "primary", "variant": "contained"}, "Hello World!")


run(HelloWorld)

docs/source/reference/_static/vdom-json-schema.json METASEP
{
  "$ref": "#/definitions/element",
  "$schema": "http://json-schema.org/draft-07/schema",
  "definitions": {
    "element": {
      "dependentSchemas": {
        "error": {
          "properties": {
            "tagName": {
              "maxLength": 0
            }
          }
        }
      },
      "properties": {
        "attributes": {
          "type": "object"
        },
        "children": {
          "$ref": "#/definitions/elementChildren"
        },
        "error": {
          "type": "string"
        },
        "eventHandlers": {
          "$ref": "#/definitions/elementEventHandlers"
        },
        "importSource": {
          "$ref": "#/definitions/importSource"
        },
        "key": {
          "type": "string"
        },
        "tagName": {
          "type": "string"
        }
      },
      "required": ["tagName"],
      "type": "object"
    },
    "elementChildren": {
      "items": {
        "$ref": "#/definitions/elementOrString"
      },
      "type": "array"
    },
    "elementEventHandlers": {
      "patternProperties": {
        ".*": {
          "$ref": "#/definitions/eventHander"
        }
      },
      "type": "object"
    },
    "elementOrString": {
      "if": {
        "type": "object"
      },
      "then": {
        "$ref": "#/definitions/element"
      },
      "type": ["object", "string"]
    },
    "eventHander": {
      "properties": {
        "preventDefault": {
          "type": "boolean"
        },
        "stopPropagation": {
          "type": "boolean"
        },
        "target": {
          "type": "string"
        }
      },
      "required": ["target"],
      "type": "object"
    },
    "importSource": {
      "properties": {
        "fallback": {
          "if": {
            "not": {
              "type": "null"
            }
          },
          "then": {
            "$ref": "#/definitions/elementOrString"
          },
          "type": ["object", "string", "null"]
        },
        "source": {
          "type": "string"
        },
        "sourceType": {
          "enum": ["URL", "NAME"]
        },
        "unmountBeforeUpdate": {
          "type": "boolean"
        }
      },
      "required": ["source"],
      "type": "object"
    }
  }
}

docs/source/guides/understanding-reactpy/writing-tests.rst METASEP
.. _Writing Tests:

Writing Tests 🚧
================

.. note::

    Under construction 🚧

docs/source/guides/understanding-reactpy/why-reactpy-needs-keys.rst METASEP
.. _Why ReactPy Needs Keys:

Why ReactPy Needs Keys 🚧
=========================

.. note::

    Under construction 🚧

docs/source/guides/understanding-reactpy/what-are-components.rst METASEP
.. _What Are Components:

What Are Components? 🚧
=======================

.. note::

    Under construction 🚧

docs/source/guides/understanding-reactpy/the-rendering-process.rst METASEP
.. _The Rendering Process:

The Rendering Process 🚧
========================

.. refer to https://beta.reactjs.org/learn/render-and-commit

.. note::

    Under construction 🚧

docs/source/guides/understanding-reactpy/the-rendering-pipeline.rst METASEP
.. _The Rendering Pipeline:

The Rendering Pipeline 🚧
=========================

.. talk about layouts and dispatchers

.. note::

    Under construction 🚧

docs/source/guides/understanding-reactpy/representing-html.rst METASEP
.. _Representing HTML:

Representing HTML 🚧
====================

.. note::

    Under construction 🚧

We've already discussed how to contruct HTML with ReactPy in a :ref:`previous section <HTML
with ReactPy>`, but we skimmed over the question of the data structure we use to represent
it. Let's reconsider the examples from before - on the top is some HTML and on the
bottom is the corresponding code to create it in ReactPy:

.. code-block:: html

    <div>
        <h1>My Todo List</h1>
        <ul>
            <li>Build a cool new app</li>
            <li>Share it with the world!</li>
        </ul>
    </div>

.. testcode::

    from reactpy import html

    layout = html.div(
        html.h1("My Todo List"),
        html.ul(
            html.li("Build a cool new app"),
            html.li("Share it with the world!"),
        )
    )

Since we've captured our HTML into out the ``layout`` variable, we can inspect what it
contains. And, as it turns out, it holds a dictionary. Printing it produces the
following output:

.. testsetup::

    from pprint import pprint
    print = lambda *args, **kwargs: pprint(*args, **kwargs, sort_dicts=False)

.. testcode::

    assert layout == {
        'tagName': 'div',
        'children': [
            {
                'tagName': 'h1',
                'children': ['My Todo List']
            },
            {
                'tagName': 'ul',
                'children': [
                    {'tagName': 'li', 'children': ['Build a cool new app']},
                    {'tagName': 'li', 'children': ['Share it with the world!']}
                ]
            }
        ]
    }

This may look complicated, but let's take a moment to consider what's going on here. We
have a series of nested dictionaries that, in some way, represents the HTML structure
given above. If we look at their contents we should see a common form. Each has a
``tagName`` key which contains, as the name would suggest, the tag name of an HTML
element. Then within the ``children`` key is a list that either contains strings or
other dictionaries that represent HTML elements.

What we're seeing here is called a "virtual document object model" or :ref:`VDOM`. This
is just a fancy way of saying we have a representation of the document object model or
`DOM
<https://en.wikipedia.org/wiki/Document_Object_Model#:~:text=The%20Document%20Object%20Model%20(DOM,document%20with%20a%20logical%20tree.&text=Nodes%20can%20have%20event%20handlers%20attached%20to%20them.>`__
that is not the actual DOM.

docs/source/guides/understanding-reactpy/layout-render-servers.rst METASEP
.. _Layout Render Servers:

Layout Render Servers 🚧
========================

.. note::

    Under construction 🚧

docs/source/guides/understanding-reactpy/index.rst METASEP
Understanding ReactPy
=====================

.. toctree::
    :hidden:

    representing-html
    what-are-components
    the-rendering-pipeline
    why-reactpy-needs-keys
    the-rendering-process
    layout-render-servers
    writing-tests

.. note::

    Under construction 🚧

docs/source/guides/managing-state/index.rst METASEP
Managing State
==============

.. toctree::
    :hidden:

    how-to-structure-state/index
    sharing-component-state/index
    when-and-how-to-reset-state/index
    simplifying-updates-with-reducers/index
    deeply-sharing-state-with-contexts/index
    combining-contexts-and-reducers/index

.. dropdown:: :octicon:`bookmark-fill;2em` What You'll Learn
    :color: info
    :animate: fade-in
    :open:

    .. grid:: 1 2 2 2

        .. grid-item-card:: :octicon:`organization` How to Structure State
            :link: how-to-structure-state/index
            :link-type: doc

            Make it easy to reason about your application with strategies for organizing
            state.

        .. grid-item-card:: :octicon:`link` Sharing Component State
            :link: sharing-component-state/index
            :link-type: doc

            Allow components to vary vary together, by lifting state into common
            parents.

        .. grid-item-card:: :octicon:`light-bulb` When and How to Reset State
            :link: when-and-how-to-reset-state/index
            :link-type: doc

            Control if and how state is preserved by understanding it's relationship to
            the "UI tree".

        .. grid-item-card:: :octicon:`plug` Simplifying Updates with Reducers
            :link: simplifying-updates-with-reducers/index
            :link-type: doc

            Consolidate state update logic outside your component in a single function,
            called a “reducer".

        .. grid-item-card:: :octicon:`broadcast` Deeply Sharing State with Contexts
            :link: deeply-sharing-state-with-contexts/index
            :link-type: doc

            Instead of passing shared state down deep component trees, bring state into
            "contexts" instead.

        .. grid-item-card:: :octicon:`rocket` Combining Contexts and Reducers
            :link: combining-contexts-and-reducers/index
            :link-type: doc

            You can combine reducers and context together to manage state of a complex
            screen.


Section 1: How to Structure State
---------------------------------

.. note::

    Under construction 🚧


Section 2: Shared Component State
---------------------------------

Sometimes, you want the state of two components to always change together. To do it,
remove state from both of them, move it to their closest common parent, and then pass it
down to them via props. This is known as “lifting state up”, and it’s one of the most
common things you will do writing code with ReactPy.

In the example below the search input and the list of elements below share the same
state, the state represents the food name. Note how the component ``Table`` gets called
at each change of state. The component is observing the state and reacting to state
changes automatically, just like it would do in React.

.. reactpy:: sharing-component-state/_examples/synced_inputs

.. card::
    :link: sharing-component-state/index
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    Allow components to vary vary together, by lifting state into common parents.


Section 3: When and How to Reset State
--------------------------------------

.. note::

    Under construction 🚧


Section 4: Simplifying Updates with Reducers
--------------------------------------------

.. note::

    Under construction 🚧


Section 5: Deeply Sharing State with Contexts
---------------------------------------------

.. note::

    Under construction 🚧



Section 6: Combining Contexts and Reducers
------------------------------------------

.. note::

    Under construction 🚧

docs/source/guides/getting-started/running-reactpy.rst METASEP
Running ReactPy
===============

The simplest way to run ReactPy is with the :func:`~reactpy.backend.utils.run` function. This
is the method you'll see used throughout this documentation. However, this executes your
application using a development server which is great for testing, but probably not what
if you're :ref:`deploying in production <Running ReactPy in Production>`. Below are some
more robust and performant ways of running ReactPy with various supported servers.


Running ReactPy in Production
-----------------------------

The first thing you'll need to do if you want to run ReactPy in production is choose a
backend implementation and follow its documentation on how to create and run an
application. This is the backend :ref:`you probably chose <Native Backends>` when
installing ReactPy. Then you'll need to configure that application with an ReactPy view. We
show the basics of how to set up, and then run, each supported backend below, but all
implementations will follow a pattern similar to the following:

.. code-block::

    from my_chosen_backend import Application

    from reactpy import component, html
    from reactpy.backend.my_chosen_backend import configure


    @component
    def HelloWorld():
        return html.h1("Hello, world!")


    app = Application()
    configure(app, HelloWorld)

You'll then run this ``app`` using an `ASGI <https://asgi.readthedocs.io/en/latest/>`__
or `WSGI <https://wsgi.readthedocs.io/>`__ server from the command line.


Running with `FastAPI <https://fastapi.tiangolo.com>`__
.......................................................

.. reactpy:: _examples/run_fastapi

Then assuming you put this in ``main.py``, you can run the ``app`` using the `Uvicorn
<https://www.uvicorn.org/>`__ ASGI server:

.. code-block:: bash

    uvicorn main:app


Running with `Flask <https://palletsprojects.com/p/flask/>`__
.............................................................

.. reactpy:: _examples/run_flask

Then assuming you put this in ``main.py``, you can run the ``app`` using the `Gunicorn
<https://gunicorn.org/>`__ WSGI server:

.. code-block:: bash

    gunicorn main:app


Running with `Sanic <https://sanicframework.org>`__
...................................................

.. reactpy:: _examples/run_sanic

Then assuming you put this in ``main.py``, you can run the ``app`` using Sanic's builtin
server:

.. code-block:: bash

    sanic main.app


Running with `Starlette <https://www.starlette.io/>`__
......................................................

.. reactpy:: _examples/run_starlette

Then assuming you put this in ``main.py``, you can run the application using the
`Uvicorn <https://www.uvicorn.org/>`__ ASGI server:

.. code-block:: bash

    uvicorn main:app


Running with `Tornado <https://www.tornadoweb.org/en/stable/>`__
................................................................

.. reactpy:: _examples/run_tornado

Tornado is run using it's own builtin server rather than an external WSGI or ASGI
server.


Running ReactPy in Debug Mode
-----------------------------

ReactPy provides a debug mode that is turned off by default. This can be enabled when you
run your application by setting the ``REACTPY_DEBUG_MODE`` environment variable.

.. tab-set::

    .. tab-item:: Unix Shell

        .. code-block::

            export REACTPY_DEBUG_MODE=1
            python my_reactpy_app.py

    .. tab-item:: Command Prompt

        .. code-block:: text

            set REACTPY_DEBUG_MODE=1
            python my_reactpy_app.py

    .. tab-item:: PowerShell

        .. code-block:: powershell

            $env:REACTPY_DEBUG_MODE = "1"
            python my_reactpy_app.py

.. danger::

    Leave debug mode off in production!

Among other things, running in this mode:

- Turns on debug log messages
- Adds checks to ensure the :ref:`VDOM` spec is adhered to
- Displays error messages that occur within your app

Errors will be displayed where the uppermost component is located in the view:

.. reactpy:: _examples/debug_error_example


Backend Configuration Options
-----------------------------

ReactPy's various backend implementations come with ``Options`` that can be passed to their
respective ``configure()`` functions in the following way:

.. code-block::

    from reactpy.backend.<implementation> import configure, Options

    configure(app, MyComponent, Options(...))

To learn more read about the options for your chosen backend ``<implementation>``:

- :class:`reactpy.backend.fastapi.Options`
- :class:`reactpy.backend.flask.Options`
- :class:`reactpy.backend.sanic.Options`
- :class:`reactpy.backend.starlette.Options`
- :class:`reactpy.backend.tornado.Options`


Embed in an Existing Webpage
----------------------------

ReactPy provides a Javascript client called ``@reactpy/client`` that can be used to embed
ReactPy views within an existing applications. This is actually how the interactive
examples throughout this documentation have been created. You can try this out by
embedding one the examples from this documentation into your own webpage:

.. tab-set::

    .. tab-item:: HTML

        .. literalinclude:: _static/embed-doc-ex.html
            :language: html

    .. tab-item:: ▶️ Result

        .. raw:: html
            :file: _static/embed-doc-ex.html

.. note::

    For more information on how to use the client see the :ref:`Javascript API`
    reference. Or if you need to, your can :ref:`write your own backend implementation
    <writing your own backend>`.

As mentioned though, this is connecting to the server that is hosting this
documentation. If you want to connect to a view from your own server, you'll need to
change the URL above to one you provide. One way to do this might be to add to an
existing application. Another would be to run ReactPy in an adjacent web server instance
that you coordinate with something like `NGINX <https://www.nginx.com/>`__. For the sake
of simplicity, we'll assume you do something similar to the following in an existing
Python app:

.. tab-set::

    .. tab-item:: main.py

        .. literalinclude:: _static/embed-reactpy-view/main.py
            :language: python

    .. tab-item:: index.html

        .. literalinclude:: _static/embed-reactpy-view/index.html
            :language: html

After running ``python main.py``, you should be able to navigate to
``http://127.0.0.1:8000/index.html`` and see:

.. card::
    :text-align: center

    .. image:: _static/embed-reactpy-view/screenshot.png
        :width: 500px


docs/source/guides/getting-started/installing-reactpy.rst METASEP
Installing ReactPy
==================

You will typically ``pip`` install ReactPy to alongside one of it's natively supported
backends. For example, if we want to run ReactPy using the `Starlette
<https://www.starlette.io/>`__ backend you would run

.. code-block:: bash

    pip install "reactpy[starlette]"

If you want to install a "pure" version of ReactPy without a backend implementation you can
do so without any installation extras. You might do this if you wanted to :ref:`use a
custom backend <using a custom backend>` or if you wanted to manually pin your
dependencies:

.. code-block:: bash

    pip install reactpy


Native Backends
---------------

ReactPy includes built-in support for a variety backend implementations. To install the
required dependencies for each you should substitute ``starlette`` from the ``pip
install`` command above with one of the options below:

- ``fastapi`` - https://fastapi.tiangolo.com
- ``flask`` - https://palletsprojects.com/p/flask/
- ``sanic`` - https://sanicframework.org
- ``starlette`` - https://www.starlette.io/
- ``tornado`` - https://www.tornadoweb.org/en/stable/

If you need to, you can install more than one option by separating them with commas:

.. code-block:: bash

    pip install "reactpy[fastapi,flask,sanic,starlette,tornado]"

Once this is complete you should be able to :ref:`run ReactPy <Running ReactPy>` with your
chosen implementation.


Other Backends
--------------

While ReactPy can run in a variety of contexts, sometimes frameworks require extra work in
order to integrate with them. In these cases, the ReactPy team distributes bindings for
those frameworks as separate Python packages. For documentation on how to install and
run ReactPy in these supported frameworks, follow the links below:

.. raw:: html

    <style>
        .card-logo-image {
            display: flex;
            justify-content: center;
            align-content: center;
            padding: 10px;
            background-color: var(--color-background-primary);
            border: 2px solid var(--color-background-border);
        }

        .transparent-text-color {
            color: transparent;
        }
    </style>

.. role:: transparent-text-color

.. We add transparent-text-color to the text so it's not visible, but it's still
.. searchable.

.. grid:: 3

    .. grid-item-card::
        :link: https://github.com/reactive-python/django-reactpy
        :img-background: _static/logo-django.svg
        :class-card: card-logo-image

        :transparent-text-color:`Django`

    .. grid-item-card::
        :link: https://github.com/reactive-python/reactpy-jupyter
        :img-background: _static/logo-jupyter.svg
        :class-card: card-logo-image

        :transparent-text-color:`Jupyter`

    .. grid-item-card::
        :link: https://github.com/reactive-python/reactpy-dash
        :img-background: _static/logo-plotly.svg
        :class-card: card-logo-image

        :transparent-text-color:`Plotly Dash`


For Development
---------------

If you want to contribute to the development of ReactPy or modify it, you'll want to
install a development version of ReactPy. This involves cloning the repository where ReactPy's
source is maintained, and setting up a :ref:`development environment`. From there you'll
be able to modifying ReactPy's source code and :ref:`run its tests <Running The Tests>` to
ensure the modifications you've made are backwards compatible. If you want to add a new
feature to ReactPy you should write your own test that validates its behavior.

If you have questions about how to modify ReactPy or help with its development, be sure to
:discussion:`start a discussion <new?category=question>`. The ReactPy team are always
excited to :ref:`welcome <everyone can contribute>` new contributions and contributors
of all kinds

.. card::
    :link: /about/contributor-guide
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    Learn more about how to contribute to the development of ReactPy.

docs/source/guides/getting-started/index.rst METASEP
Getting Started
===============

.. toctree::
    :hidden:

    installing-reactpy
    running-reactpy

.. dropdown:: :octicon:`bookmark-fill;2em` What You'll Learn
    :color: info
    :animate: fade-in
    :open:

    .. grid:: 1 2 2 2

        .. grid-item-card:: :octicon:`tools` Installing ReactPy
            :link: installing-reactpy
            :link-type: doc

            Learn how ReactPy can be installed in a variety of different ways - with
            different web servers and even in different frameworks.

        .. grid-item-card:: :octicon:`play` Running ReactPy
            :link: running-reactpy
            :link-type: doc

            See how ReactPy can be run with a variety of different production servers or be
            added to existing applications.

The fastest way to get started with ReactPy is to try it out in a `Juptyer Notebook
<https://mybinder.org/v2/gh/reactive-python/reactpy-jupyter/main?urlpath=lab/tree/notebooks/introduction.ipynb>`__.
If you want to use a Notebook to work through the examples shown in this documentation,
you'll need to replace calls to ``reactpy.run(App)`` with a line at the end of each cell
that constructs the ``App()`` in question. If that doesn't make sense, the introductory
notebook linked below will demonstrate how to do this:

.. card::
    :link: https://mybinder.org/v2/gh/reactive-python/reactpy-jupyter/main?urlpath=lab/tree/notebooks/introduction.ipynb

    .. image:: _static/reactpy-in-jupyterlab.gif
        :scale: 72%
        :align: center


Section 1: Installing ReactPy
-----------------------------

The next fastest option is to install ReactPy along with a supported server (like
``starlette``) with ``pip``:

.. code-block:: bash

    pip install "reactpy[starlette]"

To check that everything is working you can run the sample application:

.. code-block:: bash

    python -c "import reactpy; reactpy.run(reactpy.sample.SampleApp)"

.. note::

    This launches a simple development server which is good enough for testing, but
    probably not what you want to use in production. When deploying in production,
    there's a number of different ways of :ref:`running ReactPy <Section 2: Running ReactPy>`.

You should then see a few log messages:

.. code-block:: text

    2022-03-27T11:58:59-0700 | WARNING | You are running a development server. Change this before deploying in production!
    2022-03-27T11:58:59-0700 | INFO | Running with 'Starlette' at http://127.0.0.1:8000

The second log message includes a URL indicating where you should go to view the app.
That will usually be http://127.0.0.1:8000. Once you go to that URL you should see
something like this:

.. card::

    .. reactpy-view:: _examples/sample_app

If you get a ``RuntimeError`` similar to the following:

.. code-block:: text

    Found none of the following builtin server implementations...

Then be sure you run ``pip install "reactpy[starlette]"`` instead of just ``reactpy``. For
anything else, report your issue in ReactPy's :discussion-type:`discussion forum
<problem>`.

.. card::
    :link: installing-reactpy
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    Learn how ReactPy can be installed in a variety of different ways - with different web
    servers and even in different frameworks.


Section 2: Running ReactPy
--------------------------

Once you've :ref:`installed ReactPy <Installing ReactPy>`, you'll want to learn how to run an
application. Throughout most of the examples in this documentation, you'll see the
:func:`~reactpy.backend.utils.run` function used. While it's convenient tool for
development it shouldn't be used in production settings - it's slow, and could leak
secrets through debug log messages.

.. reactpy:: _examples/hello_world

.. card::
    :link: running-reactpy
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    See how ReactPy can be run with a variety of different production servers or be
    added to existing applications.

docs/source/guides/escape-hatches/using-a-custom-client.rst METASEP
.. _Writing Your Own Client:
.. _Using a Custom Client:

Using a Custom Client 🚧
========================

.. note::

    Under construction 🚧

docs/source/guides/escape-hatches/using-a-custom-backend.rst METASEP
.. _Writing Your Own Backend:
.. _Using a Custom Backend:

Using a Custom Backend 🚧
=========================

.. note::

    Under construction 🚧

docs/source/guides/escape-hatches/javascript-components.rst METASEP
.. _Javascript Component:

Javascript Components
=====================

While ReactPy is a great tool for displaying HTML and responding to browser events with
pure Python, there are other projects which already allow you to do this inside
`Jupyter Notebooks <https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Basics.html>`__
or in standard
`web apps <https://blog.jupyter.org/and-voil%C3%A0-f6a2c08a4a93?gi=54b835a2fcce>`__.
The real power of ReactPy comes from its ability to seamlessly leverage the existing
Javascript ecosystem. This can be accomplished in different ways for different reasons:

.. list-table::
    :header-rows: 1

    *   - Integration Method
        - Use Case

    *   - :ref:`Dynamically Loaded Components`
        - You want to **quickly experiment** with ReactPy and the Javascript ecosystem.

    *   - :ref:`Custom Javascript Components`
        - You want to create polished software that can be **easily shared** with others.


.. _Dynamically Loaded Component:

Dynamically Loaded Components
-----------------------------

.. note::

    This method is not recommended in production systems - see :ref:`Distributing
    Javascript` for more info. Instead, it's best used during exploratory phases of
    development.

ReactPy makes it easy to draft your code when you're in the early stages of development by
using a CDN_ to dynamically load Javascript packages on the fly. In this example we'll
be using the ubiquitous React-based UI framework `Material UI`_.

.. reactpy:: _examples/material_ui_button_no_action

So now that we can display a Material UI Button we probably want to make it do
something. Thankfully there's nothing new to learn here, you can pass event handlers to
the button just as you did when :ref:`getting started <responding to events>`. Thus, all
we need to do is add an ``onClick`` handler to the component:

.. reactpy:: _examples/material_ui_button_on_click


.. _Custom Javascript Component:

Custom Javascript Components
----------------------------

For projects that will be shared with others, we recommend bundling your Javascript with
Rollup_ or Webpack_ into a `web module`_. ReactPy also provides a `template repository`_
that can be used as a blueprint to build a library of React components.

To work as intended, the Javascript bundle must export a function ``bind()`` that
adheres to the following interface:

.. code-block:: typescript

    type EventData = {
        target: string;
        data: Array<any>;
    }

    type LayoutContext = {
        sendEvent(data: EventData) => void;
        loadImportSource(source: string, sourceType: "NAME" | "URL") => Module;
    }

    type bind = (node: HTMLElement, context: LayoutContext) => ({
        create(type: any, props: Object, children: Array<any>): any;
        render(element): void;
        unmount(): void;
    });

.. note::

    - ``node`` is the ``HTMLElement`` that ``render()`` should mount to.

    - ``context`` can send events back to the server and load "import sources"
      (like a custom component module).

    - ``type`` is a named export of the current module, or a string (e.g. ``"div"``,
      ``"button"``, etc.)

    - ``props`` is an object containing attributes and callbacks for the given
      ``component``.

    - ``children`` is an array of elements which were constructed by recursively calling
      ``create``.

The interface returned by ``bind()`` can be thought of as being similar to that of
React.

- ``create`` ➜ |React.createElement|_
- ``render`` ➜ |ReactDOM.render|_
- ``unmount`` ➜ |ReactDOM.unmountComponentAtNode|_

.. |React.createElement| replace:: ``React.createElement``
.. _React.createElement: https://reactjs.org/docs/react-api.html#createelement

.. |ReactDOM.render| replace:: ``ReactDOM.render``
.. _ReactDOM.render: https://reactjs.org/docs/react-dom.html#render

.. |ReactDOM.unmountComponentAtNode| replace:: ``ReactDOM.unmountComponentAtNode``
.. _ReactDOM.unmountComponentAtNode: https://reactjs.org/docs/react-api.html#createelement

It will be used in the following manner:

.. code-block:: javascript

    // once on mount
    const binding = bind(node, context);

    // on every render
    let element = binding.create(type, props, children)
    binding.render(element);

    // once on unmount
    binding.unmount();

The simplest way to try this out yourself though, is to hook in a simple hand-crafted
Javascript module that has the requisite interface. In the example to follow we'll
create a very basic SVG line chart. The catch though is that we are limited to using
Javascript that can run directly in the browser. This means we can't use fancy syntax
like `JSX <https://reactjs.org/docs/introducing-jsx.html>`__ and instead will use
`htm <https://github.com/developit/htm>`__ to simulate JSX in plain Javascript.

.. reactpy:: _examples/super_simple_chart


.. Links
.. =====

.. _Material UI: https://material-ui.com/
.. _CDN: https://en.wikipedia.org/wiki/Content_delivery_network
.. _template repository: https://github.com/reactive-python/reactpy-js-component-template
.. _web module: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
.. _Rollup: https://rollupjs.org/guide/en/
.. _Webpack: https://webpack.js.org/

docs/source/guides/escape-hatches/index.rst METASEP
Escape Hatches
==============

.. toctree::
    :hidden:

    javascript-components
    distributing-javascript
    using-a-custom-backend
    using-a-custom-client

.. note::

    Under construction 🚧

docs/source/guides/escape-hatches/distributing-javascript.rst METASEP
Distributing Javascript
=======================

There are two ways that you can distribute your :ref:`Custom Javascript Components`:

- Using a CDN_
- In a Python package via PyPI_

These options are not mutually exclusive though, and it may be beneficial to support
both options. For example, if you upload your Javascript components to NPM_ and also
bundle your Javascript inside a Python package, in principle your users can determine
which work best for them. Regardless though, either you or, if you give then the choice,
your users, will have to consider the tradeoffs of either approach.

- :ref:`Distributing Javascript via CDN_` - Most useful in production-grade applications
  where its assumed the user has a network connection. In this scenario a CDN's `edge
  network <https://en.wikipedia.org/wiki/Edge_computing>`__ can be used to bring the
  Javascript source closer to the user in order to reduce page load times.

- :ref:`Distributing Javascript via PyPI_` - This method is ideal for local usage since
  the user can server all the Javascript components they depend on from their computer
  without requiring a network connection.


Distributing Javascript via CDN_
--------------------------------

Under this approach, to simplify these instructions, we're going to ignore the problem
of distributing the Javascript since that must be handled by your CDN. For open source
or personal projects, a CDN like https://unpkg.com/ makes things easy by automatically
preparing any package that's been uploaded to NPM_. If you need to roll with your own
private CDN, this will likely be more complicated.

In either case though, on the Python side, things are quite simple. You need only pass
the URL where your package can be found to :func:`~reactpy.web.module.module_from_url`
where you can then load any of its exports:

.. code-block::

    import reactpy

    your_module = ido.web.module_from_url("https://some.cdn/your-module")
    YourComponent = reactpy.web.export(your_module, "YourComponent")


Distributing Javascript via PyPI_
---------------------------------

This can be most easily accomplished by using the `template repository`_ that's been
purpose-built for this. However, to get a better sense for its inner workings, we'll
briefly look at what's required. At a high level, we must consider how to...

1. bundle your Javascript into an `ECMAScript Module`)
2. include that Javascript bundle in a Python package
3. use it as a component in your application using ReactPy

In the descriptions to follow we'll be assuming that:

- NPM_ is the Javascript package manager
- The components are implemented with React_
- Rollup_ bundles the Javascript module
- Setuptools_ builds the Python package

To start, let's take a look at the file structure we'll be building:

.. code-block:: text

    your-project
    |-- js
    |   |-- src
    |   |   \-- index.js
    |   |-- package.json
    |   \-- rollup.config.js
    |-- your_python_package
    |   |-- __init__.py
    |   \-- widget.py
    |-- Manifest.in
    |-- pyproject.toml
    \-- setup.py

``index.js`` should contain the relevant exports (see
:ref:`Custom JavaScript Components` for more info):

.. code-block:: javascript

    import * as React from "react";
    import * as ReactDOM from "react-dom";

    export function bind(node, config) {
        return {
            create: (component, props, children) =>
                React.createElement(component, props, ...children),
            render: (element) => ReactDOM.render(element, node),
            unmount: () => ReactDOM.unmountComponentAtNode(node),
        };
    }

    // exports for your components
    export YourFirstComponent(props) {...};
    export YourSecondComponent(props) {...};
    export YourThirdComponent(props) {...};


Your ``package.json`` should include the following:

.. code-block:: python

    {
      "name": "YOUR-PACKAGE-NAME",
      "scripts": {
        "build": "rollup --config",
        ...
      },
      "devDependencies": {
        "rollup": "^2.35.1",
        "rollup-plugin-commonjs": "^10.1.0",
        "rollup-plugin-node-resolve": "^5.2.0",
        "rollup-plugin-replace": "^2.2.0",
        ...
      },
      "dependencies": {
        "react": "^17.0.1",
        "react-dom": "^17.0.1",
        "@reactpy/client": "^0.8.5",
        ...
      },
      ...
    }

Getting a bit more in the weeds now, your ``rollup.config.js`` file should be designed
such that it drops an ES Module at ``your-project/your_python_package/bundle.js`` since
we'll be writing ``widget.py`` under that assumption.

.. note::

    Don't forget to ignore this ``bundle.js`` file when committing code (with a
    ``.gitignore`` if you're using Git) since it can always rebuild from the raw
    Javascript source in ``your-project/js``.

.. code-block:: javascript

    import resolve from "rollup-plugin-node-resolve";
    import commonjs from "rollup-plugin-commonjs";
    import replace from "rollup-plugin-replace";

    export default {
      input: "src/index.js",
      output: {
        file: "../your_python_package/bundle.js",
        format: "esm",
      },
      plugins: [
        resolve(),
        commonjs(),
        replace({
          "process.env.NODE_ENV": JSON.stringify("production"),
        }),
      ]
    };

Your ``widget.py`` file should then load the neighboring bundle file using
:func:`~reactpy.web.module.module_from_file`. Then components from that bundle can be
loaded with :func:`~reactpy.web.module.export`.

.. code-block::

    from pathlib import Path

    import reactpy

    _BUNDLE_PATH = Path(__file__).parent / "bundle.js"
    _WEB_MODULE = reactpy.web.module_from_file(
        # Note that this is the same name from package.json - this must be globally
        # unique since it must share a namespace with all other javascript packages.
        name="YOUR-PACKAGE-NAME",
        file=_BUNDLE_PATH,
        # What to temporarilly display while the module is being loaded
        fallback="Loading...",
    )

    # Your module must provide a named export for YourFirstComponent
    YourFirstComponent = reactpy.web.export(_WEB_MODULE, "YourFirstComponent")

    # It's possible to export multiple components at once
    YourSecondComponent, YourThirdComponent = reactpy.web.export(
        _WEB_MODULE, ["YourSecondComponent", "YourThirdComponent"]
    )

.. note::

    When :data:`reactpy.config.REACTPY_DEBUG_MODE` is active, named exports will be validated.

The remaining files that we need to create are concerned with creating a Python package.
We won't cover all the details here, so refer to the Setuptools_ documentation for
more information. With that said, the first file to fill out is `pyproject.toml` since
we need to declare what our build tool is (in this case Setuptools):

.. code-block:: toml

    [build-system]
    requires = ["setuptools>=40.8.0", "wheel"]
    build-backend = "setuptools.build_meta"

Then, we can creat the ``setup.py`` file which uses Setuptools. This will differ
substantially from a normal ``setup.py`` file since, as part of the build process we'll
need to use NPM to bundle our Javascript. This requires customizing some of the build
commands in Setuptools like ``build``, ``sdist``, and ``develop``:

.. code-block:: python

    import subprocess
    from pathlib import Path

    from setuptools import setup, find_packages
    from distutils.command.build import build
    from distutils.command.sdist import sdist
    from setuptools.command.develop import develop

    PACKAGE_SPEC = {}  # gets passed to setup() at the end


    # -----------------------------------------------------------------------------
    # General Package Info
    # -----------------------------------------------------------------------------


    PACKAGE_NAME = "your_python_package"

    PACKAGE_SPEC.update(
        name=PACKAGE_NAME,
        version="0.0.1",
        packages=find_packages(exclude=["tests*"]),
        classifiers=["Framework :: ReactPy", ...],
        keywords=["ReactPy", "components", ...],
        # install ReactPy with this package
        install_requires=["reactpy"],
        # required in order to include static files like bundle.js using MANIFEST.in
        include_package_data=True,
        # we need access to the file system, so cannot be run from a zip file
        zip_safe=False,
    )


    # ----------------------------------------------------------------------------
    # Build Javascript
    # ----------------------------------------------------------------------------


    # basic paths used to gather files
    PROJECT_ROOT = Path(__file__).parent
    PACKAGE_DIR = PROJECT_ROOT / PACKAGE_NAME
    JS_DIR = PROJECT_ROOT / "js"


    def build_javascript_first(cls):
        class Command(cls):
            def run(self):
                for cmd_str in ["npm install", "npm run build"]:
                    subprocess.run(cmd_str.split(), cwd=str(JS_DIR), check=True)
                super().run()

        return Command


    package["cmdclass"] = {
        "sdist": build_javascript_first(sdist),
        "build": build_javascript_first(build),
        "develop": build_javascript_first(develop),
    }


    # -----------------------------------------------------------------------------
    # Run It
    # -----------------------------------------------------------------------------


    if __name__ == "__main__":
        setup(**package)


Finally, since we're using ``include_package_data`` you'll need a MANIFEST.in_ file that
includes ``bundle.js``:

.. code-block:: text

    include your_python_package/bundle.js

And that's it! While this might seem like a lot of work, you're always free to start
creating your custom components using the provided `template repository`_ so you can get
up and running as quickly as possible.


.. Links
.. =====

.. _NPM: https://www.npmjs.com
.. _install NPM: https://www.npmjs.com/get-npm
.. _CDN: https://en.wikipedia.org/wiki/Content_delivery_network
.. _PyPI: https://pypi.org/
.. _template repository: https://github.com/reactive-python/reactpy-js-component-template
.. _web module: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
.. _Rollup: https://rollupjs.org/guide/en/
.. _Webpack: https://webpack.js.org/
.. _Setuptools: https://setuptools.readthedocs.io/en/latest/userguide/index.html
.. _ECMAScript Module: https://tc39.es/ecma262/#sec-modules
.. _React: https://reactjs.org
.. _MANIFEST.in: https://packaging.python.org/guides/using-manifest-in/

docs/source/guides/creating-interfaces/index.rst METASEP
Creating Interfaces
===================

.. toctree::
    :hidden:

    html-with-reactpy/index
    your-first-components/index
    rendering-data/index

.. dropdown:: :octicon:`bookmark-fill;2em` What You'll Learn
    :color: info
    :animate: fade-in
    :open:

    .. grid:: 1 2 2 2

        .. grid-item-card:: :octicon:`code-square` HTML with ReactPy
            :link: html-with-reactpy/index
            :link-type: doc

            Construct HTML layouts from the basic units of user interface functionality.

        .. grid-item-card:: :octicon:`package` Your First Components
            :link: your-first-components/index
            :link-type: doc

            Define reusable building blocks that it easier to construct complex
            interfaces.

        .. grid-item-card:: :octicon:`database` Rendering Data
            :link: rendering-data/index
            :link-type: doc

            Use data to organize and render HTML elements and components.

ReactPy is a Python package for making user interfaces (UI). These interfaces are built
from small elements of functionality like buttons text and images. ReactPy allows you to
combine these elements into reusable, nestable :ref:`"components" <your first
components>`. In the sections that follow you'll learn how these UI elements are created
and organized into components. Then, you'll use components to customize and
conditionally display more complex UIs.


Section 1: HTML with ReactPy
----------------------------

In a typical Python-base web application the responsibility of defining the view along
with its backing data and logic are distributed between a client and server
respectively. With ReactPy, both these tasks are centralized in a single place. The most
foundational pilar of this capability is formed by allowing HTML interfaces to be
constructed in Python. Let's consider the HTML sample below:

.. code-block:: html

    <h1>My Todo List</h1>
    <ul>
        <li>Build a cool new app</li>
        <li>Share it with the world!</li>
    </ul>

To recreate the same thing in ReactPy you would write:

.. code-block::

    from reactpy import html

    html.div(
        html.h1("My Todo List"),
        html.ul(
            html.li("Design a cool new app"),
            html.li("Build it"),
            html.li("Share it with the world!"),
        )
    )

.. card::
    :link: html-with-reactpy/index
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    Construct HTML layouts from the basic units of user interface functionality.


Section 2: Your First Components
--------------------------------

The next building block in our journey with ReactPy are components. At their core,
components are just a normal Python functions that return :ref:`HTML <HTML with ReactPy>`.
The one special thing about them that we'll concern ourselves with now, is that to
create them we need to add an ``@component`` `decorator
<https://realpython.com/primer-on-python-decorators/>`__. To see what this looks like in
practice we'll quickly make a ``Photo`` component:

.. reactpy:: your-first-components/_examples/simple_photo

.. card::
    :link: your-first-components/index
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    Define reusable building blocks that it easier to construct complex interfaces.


Section 3: Rendering Data
-------------------------

The last pillar of knowledge you need before you can start making :ref:`interactive
interfaces <adding interactivity>` is the ability to render sections of the UI given a
collection of data. This will require you to understand how elements which are derived
from data in this way must be organized with :ref:`"keys" <Organizing Items With Keys>`.
One case where we might want to do this is if items in a todo list come from a list of
data that we want to sort and filter:

.. reactpy:: rendering-data/_examples/todo_list_with_keys

.. card::
    :link: rendering-data/index
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    Use data to organize and render HTML elements and components.

docs/source/guides/adding-interactivity/index.rst METASEP
Adding Interactivity
====================

.. toctree::
    :hidden:

    responding-to-events/index
    components-with-state/index
    state-as-a-snapshot/index
    multiple-state-updates/index
    dangers-of-mutability/index


.. dropdown:: :octicon:`bookmark-fill;2em` What You'll Learn
    :color: info
    :animate: fade-in
    :open:

    .. grid:: 1 2 2 2

        .. grid-item-card:: :octicon:`bell` Responding to Events
            :link: responding-to-events/index
            :link-type: doc

            Define event handlers and learn about the available event types they can be
            bound to.

        .. grid-item-card:: :octicon:`package-dependencies` Components With State
            :link: components-with-state/index
            :link-type: doc

            Allow components to change what they display by saving and updating their
            state.

        .. grid-item-card:: :octicon:`device-camera-video` State as a Snapshot
            :link: state-as-a-snapshot/index
            :link-type: doc

            Learn why state updates schedules a re-render, instead of being applied
            immediately.

        .. grid-item-card:: :octicon:`versions` Multiple State Updates
            :link: multiple-state-updates/index
            :link-type: doc

            Learn how updates to a components state can be batched, or applied
            incrementally.

        .. grid-item-card:: :octicon:`issue-opened` Dangers of Mutability
            :link: dangers-of-mutability/index
            :link-type: doc

            See the pitfalls of working with mutable data types and how to avoid them.


Section 1: Responding to Events
-------------------------------

ReactPy lets you add event handlers to your parts of the interface. This means that you can
define synchronous or asynchronous functions that are triggered when a particular user
interaction occurs like clicking, hovering, of focusing on form inputs, and more.

.. reactpy:: responding-to-events/_examples/button_prints_message

It may feel weird to define a function within a function like this, but doing so allows
the ``handle_event`` function to access information from within the scope of the
component. That's important if you want to use any arguments that may have beend passed
your component in the handler.

.. card::
    :link: responding-to-events/index
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    Define event handlers and learn about the available event types they can be bound
    to.


Section 2: Components with State
--------------------------------

Components often need to change what’s on the screen as a result of an interaction. For
example, typing into the form should update the input field, clicking a “Comment” button
should bring up a text input field, clicking “Buy” should put a product in the shopping
cart. Components need to “remember” things like the current input value, the current
image, the shopping cart. In ReactPy, this kind of component-specific memory is created and
updated with a "hook" called ``use_state()`` that creates a **state variable** and
**state setter** respectively:

.. reactpy:: components-with-state/_examples/adding_state_variable

In ReactPy, ``use_state``, as well as any other function whose name starts with ``use``, is
called a "hook". These are special functions that should only be called while ReactPy is
:ref:`rendering <the rendering process>`. They let you "hook into" the different
capabilities of ReactPy's components of which ``use_state`` is just one (well get into the
other :ref:`later <managing state>`).

.. card::
    :link: components-with-state/index
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    Allow components to change what they display by saving and updating their state.


Section 3: State as a Snapshot
------------------------------

As we :ref:`learned earlier <Components with State>`, state setters behave a little
differently than you might exepct at first glance. Instead of updating your current
handle on the setter's corresponding variable, it schedules a re-render of the component
which owns the state.

.. code-block::

    count, set_count = use_state(0)
    print(count)  # prints: 0
    set_count(count + 1)  # schedule a re-render where count is 1
    print(count)  # still prints: 0

This behavior of ReactPy means that each render of a component is like taking a snapshot of
the UI based on the component's state at that time. Treating state in this way can help
reduce subtle bugs. For instance, in the code below there's a simple chat app with a
message input and recipient selector. The catch is that the message actually gets sent 5
seconds after the "Send" button is clicked. So what would happen if we changed the
recipient between the time the "Send" button was clicked and the moment the message is
actually sent?

.. reactpy:: state-as-a-snapshot/_examples/print_chat_message

As it turns out, changing the message recipient after pressing send does not change
where the message ulitmately goes. However, one could imagine a bug where the recipient
of a message is determined at the time the message is sent rather than at the time the
"Send" button it clicked. Thus changing the recipient after pressing send would change
where the message got sent.

In many cases, ReactPy avoids this class of bug entirely because it treats state as a
snapshot.

.. card::
    :link: state-as-a-snapshot/index
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    Learn why state updates schedules a re-render, instead of being applied immediately.


Section 4: Multiple State Updates
---------------------------------

As we saw in an earlier example, :ref:`setting state triggers renders`. In other words,
changes to state only take effect in the next render, not in the current one. Further,
changes to state are batched, calling a particular state setter 3 times won't trigger 3
renders, it will only trigger 1. This means that multiple state assignments are batched
- so long as the event handler is synchronous (i.e. the event handler is not an
``async`` function), ReactPy waits until all the code in an event handler has run before
processing state and starting the next render:

.. reactpy:: multiple-state-updates/_examples/set_color_3_times

Sometimes though, you need to update a state variable more than once before the next
render. In these cases, instead of having updates batched, you instead want them to be
applied incrementally. That is, the next update can be made to depend on the prior one.
To accomplish this, instead of passing the next state value directly (e.g.
``set_state(new_state)``), we may pass an **"updater function"** of the form
``compute_new_state(old_state)`` to the state setter (e.g.
``set_state(compute_new_state)``):

.. reactpy:: multiple-state-updates/_examples/set_state_function

.. card::
    :link: multiple-state-updates/index
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    Learn how updates to a components state can be batched, or applied incrementally.


Section 5: Dangers of Mutability
--------------------------------

While state can hold any type of value, you should be careful to avoid directly
modifying objects that you declare as state with ReactPy. In other words, you must not
:ref:`"mutate" <What is a Mutation>` values which are held as state. Rather, to change
these values you should use new ones or create copies.

This is because ReactPy does not understand that when a value is mutated, it may have
changed. As a result, mutating values will not trigger re-renders. Thus, you must be
careful to avoid mutation whenever you want ReactPy to re-render a component. For example,
instead of mutating dictionaries to update their items you should instead create a
copy that contains the desired changes:

.. reactpy:: dangers-of-mutability/_examples/dict_update

.. card::
    :link: dangers-of-mutability/index
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    See the pitfalls of working with mutable data types and how to avoid them.

docs/source/_static/css/widget-output-css-overrides.css METASEP
.widget-container h1,
.widget-container h2,
.widget-container h3,
.widget-container h4,
.widget-container h5,
.widget-container h6 {
  margin: 0 !important;
}

docs/source/_static/css/sphinx-design-overrides.css METASEP
.sd-card-body {
  display: flex;
  flex-direction: column;
  align-items: stretch;
}

.sd-tab-content .highlight pre {
  max-height: 700px;
  overflow: auto;
}

.sd-card-title .sd-badge {
  font-size: 1em;
}

docs/source/_static/css/reactpy-view.css METASEP
.interactive {
  -webkit-transition: 0.1s ease-out;
  -moz-transition: 0.1s ease-out;
  -o-transition: 0.1s ease-out;
  transition: 0.1s ease-out;
}
.widget-container {
  padding: 15px;
  overflow: auto;
  background-color: var(--color-code-background);
  min-height: 75px;
}

.widget-container .printout {
  margin-top: 20px;
  border-top: solid 2px var(--color-foreground-border);
  padding-top: 20px;
}

.widget-container > div {
  width: 100%;
}

.enable-widget-button {
  padding: 10px;
  color: #ffffff !important;
  text-transform: uppercase;
  text-decoration: none;
  background: #526cfe;
  border: 2px solid #526cfe !important;
  transition: all 0.1s ease 0s;
  box-shadow: 0 5px 10px var(--color-foreground-border);
}
.enable-widget-button:hover {
  color: #526cfe !important;
  background: #ffffff;
  transition: all 0.1s ease 0s;
}
.enable-widget-button:focus {
  outline: 0 !important;
  transform: scale(0.98);
  transition: all 0.1s ease 0s;
}

docs/source/_static/css/larger-headings.css METASEP
h1,
h2,
h3,
h4,
h5,
h6 {
  margin-top: 1.5em !important;
  font-weight: 900 !important;
}

docs/source/_static/css/larger-api-margins.css METASEP
:is(.data, .function, .class, .exception).py {
  margin-top: 3em;
}

:is(.attribute, .method).py {
  margin-top: 1.8em;
}

docs/source/_static/css/furo-theme-overrides.css METASEP
.sidebar-container {
  width: 18em;
}
.sidebar-brand-text {
  display: none;
}

docs/source/_custom_js/src/index.js METASEP
import { SimpleReactPyClient, mount } from "@reactpy/client";

let didMountDebug = false;

export function mountWidgetExample(
  mountID,
  viewID,
  reactpyServerHost,
  useActivateButton,
) {
  let reactpyHost, reactpyPort;
  if (reactpyServerHost) {
    [reactpyHost, reactpyPort] = reactpyServerHost.split(":", 2);
  } else {
    reactpyHost = window.location.hostname;
    reactpyPort = window.location.port;
  }

  const client = new SimpleReactPyClient({
    serverLocation: {
      url: `${window.location.protocol}//${reactpyHost}:${reactpyPort}`,
      route: "/",
      query: `?view_id=${viewID}`,
    },
  });

  const mountEl = document.getElementById(mountID);
  let isMounted = false;
  triggerIfInViewport(mountEl, () => {
    if (!isMounted) {
      activateView(mountEl, client, useActivateButton);
      isMounted = true;
    }
  });
}

function activateView(mountEl, client, useActivateButton) {
  if (!useActivateButton) {
    mount(mountEl, client);
    return;
  }

  const enableWidgetButton = document.createElement("button");
  enableWidgetButton.appendChild(document.createTextNode("Activate"));
  enableWidgetButton.setAttribute("class", "enable-widget-button");

  enableWidgetButton.addEventListener("click", () =>
    fadeOutElementThenCallback(enableWidgetButton, () => {
      {
        mountEl.removeChild(enableWidgetButton);
        mountEl.setAttribute("class", "interactive widget-container");
        mountWithLayoutServer(mountEl, serverInfo);
      }
    }),
  );

  function fadeOutElementThenCallback(element, callback) {
    {
      var op = 1; // initial opacity
      var timer = setInterval(function () {
        {
          if (op < 0.001) {
            {
              clearInterval(timer);
              element.style.display = "none";
              callback();
            }
          }
          element.style.opacity = op;
          element.style.filter = "alpha(opacity=" + op * 100 + ")";
          op -= op * 0.5;
        }
      }, 50);
    }
  }

  mountEl.appendChild(enableWidgetButton);
}

function triggerIfInViewport(element, callback) {
  const observer = new window.IntersectionObserver(
    ([entry]) => {
      if (entry.isIntersecting) {
        callback();
      }
    },
    {
      root: null,
      threshold: 0.1, // set offset 0.1 means trigger if atleast 10% of element in viewport
    },
  );

  observer.observe(element);
}

docs/source/reference/_examples/victory_chart.py METASEP
import reactpy


victory = reactpy.web.module_from_template("react", "victory-bar", fallback="⌛")
VictoryBar = reactpy.web.export(victory, "VictoryBar")

bar_style = {"parent": {"width": "500px"}, "data": {"fill": "royalblue"}}
reactpy.run(reactpy.component(lambda: VictoryBar({"style": bar_style})))

docs/source/reference/_examples/use_state_counter.py METASEP
import reactpy


def increment(last_count):
    return last_count + 1


def decrement(last_count):
    return last_count - 1


@reactpy.component
def Counter():
    initial_count = 0
    count, set_count = reactpy.hooks.use_state(initial_count)
    return reactpy.html.div(
        f"Count: {count}",
        reactpy.html.button(
            {"on_click": lambda event: set_count(initial_count)}, "Reset"
        ),
        reactpy.html.button({"on_click": lambda event: set_count(increment)}, "+"),
        reactpy.html.button({"on_click": lambda event: set_count(decrement)}, "-"),
    )


reactpy.run(Counter)

docs/source/reference/_examples/use_reducer_counter.py METASEP
import reactpy


def reducer(count, action):
    if action == "increment":
        return count + 1
    elif action == "decrement":
        return count - 1
    elif action == "reset":
        return 0
    else:
        raise ValueError(f"Unknown action '{action}'")


@reactpy.component
def Counter():
    count, dispatch = reactpy.hooks.use_reducer(reducer, 0)
    return reactpy.html.div(
        f"Count: {count}",
        reactpy.html.button({"on_click": lambda event: dispatch("reset")}, "Reset"),
        reactpy.html.button({"on_click": lambda event: dispatch("increment")}, "+"),
        reactpy.html.button({"on_click": lambda event: dispatch("decrement")}, "-"),
    )


reactpy.run(Counter)

docs/source/reference/_examples/todo.py METASEP
import reactpy


@reactpy.component
def Todo():
    items, set_items = reactpy.hooks.use_state([])

    async def add_new_task(event):
        if event["key"] == "Enter":
            set_items(items + [event["target"]["value"]])

    tasks = []

    for index, text in enumerate(items):

        async def remove_task(event, index=index):
            set_items(items[:index] + items[index + 1 :])

        task_text = reactpy.html.td(reactpy.html.p(text))
        delete_button = reactpy.html.td(
            {"on_click": remove_task}, reactpy.html.button(["x"])
        )
        tasks.append(reactpy.html.tr(task_text, delete_button))

    task_input = reactpy.html.input({"on_key_down": add_new_task})
    task_table = reactpy.html.table(tasks)

    return reactpy.html.div(
        reactpy.html.p("press enter to add a task:"),
        task_input,
        task_table,
    )


reactpy.run(Todo)

docs/source/reference/_examples/snake_game.py METASEP
import asyncio
import enum
import random
import time

import reactpy


class GameState(enum.Enum):
    init = 0
    lost = 1
    won = 2
    play = 3


@reactpy.component
def GameView():
    game_state, set_game_state = reactpy.hooks.use_state(GameState.init)

    if game_state == GameState.play:
        return GameLoop(grid_size=6, block_scale=50, set_game_state=set_game_state)

    start_button = reactpy.html.button(
        {"on_click": lambda event: set_game_state(GameState.play)}, "Start"
    )

    if game_state == GameState.won:
        menu = reactpy.html.div(reactpy.html.h3("You won!"), start_button)
    elif game_state == GameState.lost:
        menu = reactpy.html.div(reactpy.html.h3("You lost"), start_button)
    else:
        menu = reactpy.html.div(reactpy.html.h3("Click to play"), start_button)

    menu_style = reactpy.html.style(
        """
        .snake-game-menu h3 {
            margin-top: 0px !important;
        }
        """
    )

    return reactpy.html.div({"class_name": "snake-game-menu"}, menu_style, menu)


class Direction(enum.Enum):
    ArrowUp = (0, -1)
    ArrowLeft = (-1, 0)
    ArrowDown = (0, 1)
    ArrowRight = (1, 0)


@reactpy.component
def GameLoop(grid_size, block_scale, set_game_state):
    # we `use_ref` here to capture the latest direction press without any delay
    direction = reactpy.hooks.use_ref(Direction.ArrowRight.value)
    # capture the last direction of travel that was rendered
    last_direction = direction.current

    snake, set_snake = reactpy.hooks.use_state(
        [(grid_size // 2 - 1, grid_size // 2 - 1)]
    )
    food, set_food = use_snake_food(grid_size, snake)

    grid = create_grid(grid_size, block_scale)

    @reactpy.event(prevent_default=True)
    def on_direction_change(event):
        if hasattr(Direction, event["key"]):
            maybe_new_direction = Direction[event["key"]].value
            direction_vector_sum = tuple(
                map(sum, zip(last_direction, maybe_new_direction))
            )
            if direction_vector_sum != (0, 0):
                direction.current = maybe_new_direction

    grid_wrapper = reactpy.html.div({"on_key_down": on_direction_change}, grid)

    assign_grid_block_color(grid, food, "blue")

    for location in snake:
        assign_grid_block_color(grid, location, "white")

    new_game_state = None
    if snake[-1] in snake[:-1]:
        assign_grid_block_color(grid, snake[-1], "red")
        new_game_state = GameState.lost
    elif len(snake) == grid_size**2:
        assign_grid_block_color(grid, snake[-1], "yellow")
        new_game_state = GameState.won

    interval = use_interval(0.5)

    @reactpy.hooks.use_effect
    async def animate():
        if new_game_state is not None:
            await asyncio.sleep(1)
            set_game_state(new_game_state)
            return

        await interval

        new_snake_head = (
            # grid wraps due to mod op here
            (snake[-1][0] + direction.current[0]) % grid_size,
            (snake[-1][1] + direction.current[1]) % grid_size,
        )

        if snake[-1] == food:
            set_food()
            new_snake = snake + [new_snake_head]
        else:
            new_snake = snake[1:] + [new_snake_head]

        set_snake(new_snake)

    return grid_wrapper


def use_snake_food(grid_size, current_snake):
    grid_points = {(x, y) for x in range(grid_size) for y in range(grid_size)}
    points_not_in_snake = grid_points.difference(current_snake)

    food, _set_food = reactpy.hooks.use_state(current_snake[-1])

    def set_food():
        _set_food(random.choice(list(points_not_in_snake)))

    return food, set_food


def use_interval(rate):
    usage_time = reactpy.hooks.use_ref(time.time())

    async def interval() -> None:
        await asyncio.sleep(rate - (time.time() - usage_time.current))
        usage_time.current = time.time()

    return asyncio.ensure_future(interval())


def create_grid(grid_size, block_scale):
    return reactpy.html.div(
        {
            "style": {
                "height": f"{block_scale * grid_size}px",
                "width": f"{block_scale * grid_size}px",
                "cursor": "pointer",
                "display": "grid",
                "grid-gap": 0,
                "grid-template-columns": f"repeat({grid_size}, {block_scale}px)",
                "grid-template-rows": f"repeat({grid_size}, {block_scale}px)",
            },
            "tab_index": -1,
        },
        [
            reactpy.html.div(
                {"style": {"height": f"{block_scale}px"}, "key": i},
                [
                    create_grid_block("black", block_scale, key=i)
                    for i in range(grid_size)
                ],
            )
            for i in range(grid_size)
        ],
    )


def create_grid_block(color, block_scale, key):
    return reactpy.html.div(
        {
            "style": {
                "height": f"{block_scale}px",
                "width": f"{block_scale}px",
                "background_color": color,
                "outline": "1px solid grey",
            },
            "key": key,
        }
    )


def assign_grid_block_color(grid, point, color):
    x, y = point
    block = grid["children"][x]["children"][y]
    block["attributes"]["style"]["backgroundColor"] = color


reactpy.run(GameView)

docs/source/reference/_examples/slideshow.py METASEP
import reactpy


@reactpy.component
def Slideshow():
    index, set_index = reactpy.hooks.use_state(0)

    def next_image(event):
        set_index(index + 1)

    return reactpy.html.img(
        {
            "src": f"https://picsum.photos/id/{index}/800/300",
            "style": {"cursor": "pointer"},
            "on_click": next_image,
        }
    )


reactpy.run(Slideshow)

docs/source/reference/_examples/simple_dashboard.py METASEP
import asyncio
import random
import time

import reactpy
from reactpy.widgets import Input


victory = reactpy.web.module_from_template(
    "react",
    "victory-line",
    fallback="⌛",
    # not usually required (see issue #461 for more info)
    unmount_before_update=True,
)
VictoryLine = reactpy.web.export(victory, "VictoryLine")


@reactpy.component
def RandomWalk():
    mu = reactpy.hooks.use_ref(0)
    sigma = reactpy.hooks.use_ref(1)

    return reactpy.html.div(
        RandomWalkGraph(mu, sigma),
        reactpy.html.style(
            """
            .number-input-container {margin-bottom: 20px}
            .number-input-container input {width: 48%;float: left}
            .number-input-container input + input {margin-left: 4%}
            """
        ),
        NumberInput(
            "Mean",
            mu.current,
            mu.set_current,
            (-1, 1, 0.01),
        ),
        NumberInput(
            "Standard Deviation",
            sigma.current,
            sigma.set_current,
            (0, 1, 0.01),
        ),
    )


@reactpy.component
def RandomWalkGraph(mu, sigma):
    interval = use_interval(0.5)
    data, set_data = reactpy.hooks.use_state([{"x": 0, "y": 0}] * 50)

    @reactpy.hooks.use_effect
    async def animate():
        await interval
        last_data_point = data[-1]
        next_data_point = {
            "x": last_data_point["x"] + 1,
            "y": last_data_point["y"] + random.gauss(mu.current, sigma.current),
        }
        set_data(data[1:] + [next_data_point])

    return VictoryLine(
        {
            "data": data,
            "style": {
                "parent": {"width": "100%"},
                "data": {"stroke": "royalblue"},
            },
        }
    )


@reactpy.component
def NumberInput(label, value, set_value_callback, domain):
    minimum, maximum, step = domain
    attrs = {"min": minimum, "max": maximum, "step": step}

    value, set_value = reactpy.hooks.use_state(value)

    def update_value(value):
        set_value(value)
        set_value_callback(value)

    return reactpy.html.fieldset(
        {"class_name": "number-input-container"},
        reactpy.html.legend({"style": {"font-size": "medium"}}, label),
        Input(update_value, "number", value, attributes=attrs, cast=float),
        Input(update_value, "range", value, attributes=attrs, cast=float),
    )


def use_interval(rate):
    usage_time = reactpy.hooks.use_ref(time.time())

    async def interval() -> None:
        await asyncio.sleep(rate - (time.time() - usage_time.current))
        usage_time.current = time.time()

    return asyncio.ensure_future(interval())


reactpy.run(RandomWalk)

docs/source/reference/_examples/pigeon_maps.py METASEP
import reactpy


pigeon_maps = reactpy.web.module_from_template("react", "pigeon-maps", fallback="⌛")
Map, Marker = reactpy.web.export(pigeon_maps, ["Map", "Marker"])


@reactpy.component
def MapWithMarkers():
    marker_anchor, add_marker_anchor, remove_marker_anchor = use_set()

    markers = list(
        map(
            lambda anchor: Marker(
                {
                    "anchor": anchor,
                    "onClick": lambda: remove_marker_anchor(anchor),
                },
                key=str(anchor),
            ),
            marker_anchor,
        )
    )

    return Map(
        {
            "defaultCenter": (37.774, -122.419),
            "defaultZoom": 12,
            "height": "300px",
            "metaWheelZoom": True,
            "onClick": lambda event: add_marker_anchor(tuple(event["latLng"])),
        },
        markers,
    )


def use_set(initial_value=None):
    values, set_values = reactpy.hooks.use_state(initial_value or set())

    def add_value(lat_lon):
        set_values(values.union({lat_lon}))

    def remove_value(lat_lon):
        set_values(values.difference({lat_lon}))

    return values, add_value, remove_value


reactpy.run(MapWithMarkers)

docs/source/reference/_examples/network_graph.py METASEP
import random

import reactpy


react_cytoscapejs = reactpy.web.module_from_template(
    "react",
    "react-cytoscapejs",
    fallback="⌛",
)
Cytoscape = reactpy.web.export(react_cytoscapejs, "default")


@reactpy.component
def RandomNetworkGraph():
    return Cytoscape(
        {
            "style": {"width": "100%", "height": "200px"},
            "elements": random_network(20),
            "layout": {"name": "cose"},
        }
    )


def random_network(number_of_nodes):
    conns = []
    nodes = [{"data": {"id": 0, "label": 0}}]

    for src_node_id in range(1, number_of_nodes + 1):
        tgt_node = random.choice(nodes)
        src_node = {"data": {"id": src_node_id, "label": src_node_id}}

        new_conn = {"data": {"source": src_node_id, "target": tgt_node["data"]["id"]}}

        nodes.append(src_node)
        conns.append(new_conn)

    return nodes + conns


reactpy.run(RandomNetworkGraph)

docs/source/reference/_examples/matplotlib_plot.py METASEP
from io import BytesIO

import matplotlib.pyplot as plt

import reactpy
from reactpy.widgets import image


@reactpy.component
def PolynomialPlot():
    coefficients, set_coefficients = reactpy.hooks.use_state([0])

    x = [n for n in linspace(-1, 1, 50)]
    y = [polynomial(value, coefficients) for value in x]

    return reactpy.html.div(
        plot(f"{len(coefficients)} Term Polynomial", x, y),
        ExpandableNumberInputs(coefficients, set_coefficients),
    )


@reactpy.component
def ExpandableNumberInputs(values, set_values):
    inputs = []
    for i in range(len(values)):

        def set_value_at_index(event, index=i):
            new_value = float(event["target"]["value"] or 0)
            set_values(values[:index] + [new_value] + values[index + 1 :])

        inputs.append(poly_coef_input(i + 1, set_value_at_index))

    def add_input():
        set_values(values + [0])

    def del_input():
        set_values(values[:-1])

    return reactpy.html.div(
        reactpy.html.div(
            "add/remove term:",
            reactpy.html.button({"on_click": lambda event: add_input()}, "+"),
            reactpy.html.button({"on_click": lambda event: del_input()}, "-"),
        ),
        inputs,
    )


def plot(title, x, y):
    fig, axes = plt.subplots()
    axes.plot(x, y)
    axes.set_title(title)
    buffer = BytesIO()
    fig.savefig(buffer, format="png")
    plt.close(fig)
    return image("png", buffer.getvalue())


def poly_coef_input(index, callback):
    return reactpy.html.div(
        {"style": {"margin-top": "5px"}, "key": index},
        reactpy.html.label(
            "C",
            reactpy.html.sub(index),
            " × X",
            reactpy.html.sup(index),
        ),
        reactpy.html.input({"type": "number", "on_change": callback}),
    )


def polynomial(x, coefficients):
    return sum(c * (x ** (i + 1)) for i, c in enumerate(coefficients))


def linspace(start, stop, n):
    if n == 1:
        yield stop
        return
    h = (stop - start) / (n - 1)
    for i in range(n):
        yield start + h * i


reactpy.run(PolynomialPlot)

docs/source/reference/_examples/material_ui_switch.py METASEP
import reactpy


mui = reactpy.web.module_from_template("react", "@material-ui/core@^5.0", fallback="⌛")
Switch = reactpy.web.export(mui, "Switch")


@reactpy.component
def DayNightSwitch():
    checked, set_checked = reactpy.hooks.use_state(False)

    return reactpy.html.div(
        Switch(
            {
                "checked": checked,
                "onChange": lambda event, checked: set_checked(checked),
            }
        ),
        "🌞" if checked else "🌚",
    )


reactpy.run(DayNightSwitch)

docs/source/reference/_examples/click_count.py METASEP
import reactpy


@reactpy.component
def ClickCount():
    count, set_count = reactpy.hooks.use_state(0)

    return reactpy.html.button(
        {"on_click": lambda event: set_count(count + 1)}, [f"Click count: {count}"]
    )


reactpy.run(ClickCount)

docs/source/reference/specifications.rst METASEP
Specifications
==============

Describes various data structures and protocols used to define and communicate virtual
document object models (:ref:`VDOM`). The definitions below follow in the footsteps of
`a specification <https://github.com/nteract/vdom/blob/master/docs/mimetype-spec.md>`_
created by `Nteract <https://nteract.io>`_ and which was built into
`JupyterLab <https://jupyterlab.readthedocs.io/en/stable/>`_. While ReactPy's specification
for VDOM is fairly well established, it should not be relied until it's been fully
adopted by the aforementioned organizations.


VDOM
----

A set of definitions that explain how ReactPy creates a virtual representation of
the document object model. We'll begin by looking at a bit of HTML that we'll convert
into its VDOM representation:

.. code-block:: html

    <div>
      Put your name here:
      <input
        type="text"
        minlength="4"
        maxlength="8"
        onchange="a_python_callback(event)"
      />
    </div>

.. note::

  For context, the following Python code would generate the HTML above:

  .. code-block:: python

      import reactpy

      async def a_python_callback(new):
          ...

      name_input_view = reactpy.html.div(
          reactpy.html.input(
              {
                  "type": "text",
                  "minLength": 4,
                  "maxLength": 8,
                  "onChange": a_python_callback,
              }
          ),
          ["Put your name here: "],
      )

We'll take this step by step in order to show exactly where each piece of the VDOM
model comes from. To get started we'll convert the outer ``<div/>``:

.. code-block:: python

    {
        "tagName": "div",
        "children": [
            "To perform an action",
            ...
        ],
        "attributes": {},
        "eventHandlers": {}
    }

.. note::

    As we move though our conversation we'll be using ``...`` to fill in places that we
    haven't converted yet.

In this simple case, all we've done is take the name of the HTML element (``div`` in
this case) and inserted it into the ``tagName`` field of a dictionary. Then we've taken
the inner HTML and added to a list of children where the text ``"to perform an action"``
has been made into a string, and the inner ``input`` (yet to be converted) will be
expanded out into its own VDOM representation. Since the outer ``div`` is pretty simple
there aren't any ``attributes`` or ``eventHandlers``.

No we come to the inner ``input``. If we expand this out now we'll get the following:

.. code-block:: python

    {
        "tagName": "div",
        "children": [
            "To perform an action",
            {
                "tagName": "input",
                "children": [],
                "attributes": {
                    "type": "text",
                    "minLength": 4,
                    "maxLength": 8
                },
                "eventHandlers": ...
            }
        ],
        "attributes": {},
        "eventHandlers": {}
    }

Here we've had to add some attributes to our VDOM. Take note of the differing
capitalization - instead of using all lowercase (an HTML convention) we've used
`camelCase <https://en.wikipedia.org/wiki/Camel_case>`_ which is very common
in JavaScript.

Last, but not least we come to the ``eventHandlers`` for the ``input``:

.. code-block:: python

    {
        "tagName": "div",
        "children": [
            "To perform an action",
            {
                "tagName": "input",
                "children": [],
                "attributes": {
                    "type": "text",
                    "minLength": 4,
                    "maxLength": 8
                },
                "eventHandlers": {
                    "onChange": {
                      "target": "unique-id-of-a_python_callback",
                      "preventDefault": False,
                      "stopPropagation": False
                    }
                }
            }
        ],
        "attributes": {},
        "eventHandlers": {}
    }

Again we've changed the all lowercase ``onchange`` into a cameCase ``onChange`` event
type name. The various properties for the ``onChange`` handler are:

- ``target``: the unique ID for a Python callback that exists in the backend.

- ``preventDefault``: Stop the event's default action. More info
  `here <https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault>`__.

- ``stopPropagation``: prevent the event from bubbling up through the DOM. More info
  `here <https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation>`__.


VDOM JSON Schema
................

To clearly describe the VDOM spec we've created a `JSON Schema <https://json-schema.org/>`_:

.. literalinclude:: _static/vdom-json-schema.json
   :language: json


JSON Patch
----------

Updates to VDOM modules are sent using the `JSON Patch`_ specification.

... this section is still Under construction 🚧


.. Links
.. =====
.. _JSON Patch: http://jsonpatch.com/

docs/source/reference/javascript-api.rst METASEP
.. _Javascript API:

Javascript API 🚧
=================

.. note::

    Under construction 🚧

docs/source/reference/html-attributes.rst METASEP
.. testcode::

    from reactpy import html


HTML Attributes
===============

In ReactPy, HTML attributes are specified using snake_case instead of dash-separated
words. For example, ``tabindex`` and ``margin-left`` become ``tab_index`` and
``margin_left`` respectively.


Noteable Attributes
-------------------

Some attributes in ReactPy are renamed, have special meaning, or are used differently
than in HTML.

``style``
.........

As mentioned above, instead of using a string to specify the ``style`` attribute, we use
a dictionary to describe the CSS properties we want to apply to an element. For example,
the following HTML:

.. code-block:: html

    <div style="width: 50%; margin-left: 25%;">
        <h1 style="margin-top: 0px;">My Todo List</h1>
        <ul>
            <li>Build a cool new app</li>
            <li>Share it with the world!</li>
        </ul>
    </div>

Would be written in ReactPy as:

.. testcode::

    html.div(
        {
            "style": {
                "width": "50%",
                "margin_left": "25%",
            },
        },
        html.h1(
            {
                "style": {
                    "margin_top": "0px",
                },
            },
            "My Todo List",
        ),
        html.ul(
            html.li("Build a cool new app"),
            html.li("Share it with the world!"),
        ),
    )

``class`` vs ``class_name``
...........................

In HTML, the ``class`` attribute is used to specify a CSS class for an element. In
ReactPy, this attribute is renamed to ``class_name`` to avoid conflicting with the
``class`` keyword in Python. For example, the following HTML:

.. code-block:: html

    <div class="container">
        <h1 class="title">My Todo List</h1>
        <ul class="list">
            <li class="item">Build a cool new app</li>
            <li class="item">Share it with the world!</li>
        </ul>
    </div>

Would be written in ReactPy as:

.. testcode::

    html.div(
        {"class_name": "container"},
        html.h1({"class_name": "title"}, "My Todo List"),
        html.ul(
            {"class_name": "list"},
            html.li({"class_name": "item"}, "Build a cool new app"),
            html.li({"class_name": "item"}, "Share it with the world!"),
        ),
    )

``for`` vs ``html_for``
.......................

In HTML, the ``for`` attribute is used to specify the ``id`` of the element it's
associated with. In ReactPy, this attribute is renamed to ``html_for`` to avoid
conflicting with the ``for`` keyword in Python. For example, the following HTML:

.. code-block:: html

    <div>
        <label for="todo">Todo:</label>
        <input id="todo" type="text" />
    </div>

Would be written in ReactPy as:

.. testcode::

    html.div(
        html.label({"html_for": "todo"}, "Todo:"),
        html.input({"id": "todo", "type": "text"}),
    )

``dangerously_set_inner_HTML``
..............................

This is used to set the ``innerHTML`` property of an element and should be provided a
dictionary with a single key ``__html`` whose value is the HTML to be set. It should be
used with **extreme caution** as it can lead to XSS attacks if the HTML inside isn't
trusted (for example if it comes from user input).


All Attributes
--------------

`access_key <https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/accesskey>`__
  A string. Specifies a keyboard shortcut for the element. Not generally recommended.

`aria_* <https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes>`__
  ARIA attributes let you specify the accessibility tree information for this element.
  See ARIA attributes for a complete reference. In ReactPr, all ARIA attribute names are
  exactly the same as in HTML.

`auto_capitalize <https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autocapitalize>`__
  A string. Specifies whether and how the user input should be capitalized.

`content_editable <https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable>`__
  A boolean. If true, the browser lets the user edit the rendered element directly. This
  is used to implement rich text input libraries like Lexical. ReactPr warns if you try
  to pass children to an element with ``content_editable = True`` because ReactPy will
  not be able to update its content after user edits.

`data_* <https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*>`__
  Data attributes let you attach some string data to the element, for example
  data-fruit="banana". In ReactPy, they are not commonly used because you would usually
  read data from props or state instead.

`dir <https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/dir>`__
  Either ``"ltr"`` or ``"rtl"``. Specifies the text direction of the element.

`draggable <https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/draggable>`__
  A boolean. Specifies whether the element is draggable. Part of HTML Drag and Drop API.

`enter_key_hint <https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/enterkeyhint>`__
  A string. Specifies which action to present for the enter key on virtual keyboards.

`hidden <https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/hidden>`__
  A boolean or a string. Specifies whether the element should be hidden.

- `id <https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id>`__:
  A string. Specifies a unique identifier for this element, which can be used to find it
  later or connect it with other elements. Generate it with useId to avoid clashes
  between multiple instances of the same component.

`is <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-is>`__
  A string. If specified, the component will behave like a custom element.

`input_mode <https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode>`__
  A string. Specifies what kind of keyboard to display (for example, text, number, or telephone).

`item_prop <https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/itemprop>`__
  A string. Specifies which property the element represents for structured data crawlers.

`lang <https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang>`__
  A string. Specifies the language of the element.

`role <https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/role>`__
  A string. Specifies the element role explicitly for assistive technologies.

`slot <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot>`__
  A string. Specifies the slot name when using shadow DOM. In ReactPy, an equivalent
  pattern is typically achieved by passing JSX as props, for example
  ``<Layout left={<Sidebar />} right={<Content />} />``.

`spell_check <https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/spellcheck>`__
  A boolean or null. If explicitly set to true or false, enables or disables spellchecking.

`tab_index <https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex>`__
  A number. Overrides the default Tab button behavior. Avoid using values other than -1 and 0.

`title <https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/title>`__
  A string. Specifies the tooltip text for the element.

`translate <https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/translate>`__
  Either 'yes' or 'no'. Passing 'no' excludes the element content from being translated.

docs/source/reference/hooks-api.rst METASEP
=========
Hooks API
=========

Hooks are functions that allow you to "hook into" the life cycle events and state of
Components. Their usage should always follow the :ref:`Rules of Hooks`. For most use
cases the :ref:`Basic Hooks` should be enough, however the remaining
:ref:`Supplementary Hooks` should fulfill less common scenarios.


Basic Hooks
===========


Use State
---------

.. code-block::

    state, set_state = use_state(initial_state)

Returns a stateful value and a function to update it.

During the first render the ``state`` will be identical to the ``initial_state`` passed
as the first argument. However in subsequent renders ``state`` will take on the value
passed to ``set_state``.

.. code-block::

    set_state(new_state)

The ``set_state`` function accepts a ``new_state`` as its only argument and schedules a
re-render of the component where ``use_state`` was initially called. During these
subsequent re-renders the ``state`` returned by ``use_state`` will take on the value
of ``new_state``.

.. note::

    The identity of ``set_state`` is guaranteed to be preserved across renders. This
    means it can safely be omitted from dependency lists in :ref:`Use Effect` or
    :ref:`Use Callback`.


Functional Updates
..................

If the new state is computed from the previous state, you can pass a function which
accepts a single argument (the previous state) and returns the next state. Consider this
simply use case of a counter where we've pulled out logic for increment and
decremented the count:

.. reactpy:: _examples/use_state_counter

We use the functional form for the "+" and "-" buttons since the next ``count`` depends
on the previous value, while for the "Reset" button we simple assign the
``initial_count`` since it is independent of the prior ``count``. This is a trivial
example, but it demonstrates how complex state logic can be factored out into well
defined and potentially reusable functions.


Lazy Initial State
..................

In cases where it is costly to create the initial value for ``use_state``, you can pass
a constructor function that accepts no arguments instead - it will be called on the
first render of a component, but will be disregarded in all following renders:

.. code-block::

    state, set_state = use_state(lambda: some_expensive_computation(a, b, c))


Skipping Updates
................

If you update a State Hook to the same value as the current state then the component which
owns that state will not be rendered again. We check ``if new_state is current_state``
in order to determine whether there has been a change. Thus the following would not
result in a re-render:

.. code-block::

    state, set_state = use_state([])
    set_state(state)


Use Effect
----------

.. code-block::

    use_effect(did_render)

The ``use_effect`` hook accepts a function which may be imperative, or mutate state. The
function will be called immediately after the layout has fully updated.

Asynchronous actions, mutations, subscriptions, and other `side effects`_ can cause
unexpected bugs if placed in the main body of a component's render function. Thus the
``use_effect`` hook provides a way to safely escape the purely functional world of
component render functions.

.. note::

    Normally in React the ``did_render`` function is called once an update has been
    committed to the screen. Since no such action is performed by ReactPy, and the time
    at which the update is displayed cannot be known we are unable to achieve parity
    with this behavior.


Cleaning Up Effects
...................

If the effect you wish to enact creates resources, you'll probably need to clean them
up. In such cases you may simply return a function that addresses this from the
``did_render`` function which created the resource. Consider the case of opening and
then closing a connection:

.. code-block::

    def establish_connection():
        connection = open_connection()
        return lambda: close_connection(connection)

    use_effect(establish_connection)

The clean-up function will be run before the component is unmounted or, before the next
effect is triggered when the component re-renders. You can
:ref:`conditionally fire events <Conditional Effects>` to avoid triggering them each
time a component renders.


Conditional Effects
...................

By default, effects are triggered after every successful render to ensure that all state
referenced by the effect is up to date. However, when an effect function references
non-global variables, the effect will only if the value of that variable changes. For
example, imagine that we had an effect that connected to a ``url`` state variable:

.. code-block::

    url, set_url = use_state("https://example.com")

    def establish_connection():
        connection = open_connection(url)
        return lambda: close_connection(connection)

    use_effect(establish_connection)

Here, a new connection will be established whenever a new ``url`` is set.


Async Effects
.............

A behavior unique to ReactPy's implementation of ``use_effect`` is that it natively
supports ``async`` functions:

.. code-block::

    async def non_blocking_effect():
        resource = await do_something_asynchronously()
        return lambda: blocking_close(resource)

    use_effect(non_blocking_effect)


There are **three important subtleties** to note about using asynchronous effects:

1. The cleanup function must be a normal synchronous function.

2. Asynchronous effects which do not complete before the next effect is created
   following a re-render will be cancelled. This means an
   :class:`~asyncio.CancelledError` will be raised somewhere in the body of the effect.

3. An asynchronous effect may occur any time after the update which added this effect
   and before the next effect following a subsequent update.


Manual Effect Conditions
........................

In some cases, you may want to explicitly declare when an effect should be triggered.
You can do this by passing ``dependencies`` to ``use_effect``. Each of the following
values produce different effect behaviors:

- ``use_effect(..., dependencies=None)`` - triggers and cleans up on every render.
- ``use_effect(..., dependencies=[])`` - only triggers on the first and cleans up after
  the last render.
- ``use_effect(..., dependencies=[x, y])`` - triggers on the first render and on subsequent renders if
  ``x`` or ``y`` have changed.


Use Context
-----------

.. code-block::

    value = use_context(MyContext)

Accepts a context object (the value returned from
:func:`reactpy.core.hooks.create_context`) and returns the current context value for that
context. The current context value is determined by the ``value`` argument passed to the
nearest ``MyContext`` in the tree.

When the nearest <MyContext.Provider> above the component updates, this Hook will
trigger a rerender with the latest context value passed to that MyContext provider. Even
if an ancestor uses React.memo or shouldComponentUpdate, a rerender will still happen
starting at the component itself using useContext.


Supplementary Hooks
===================


Use Reducer
-----------

.. code-block::

    state, dispatch_action = use_reducer(reducer, initial_state)

An alternative and derivative of :ref:`Use State` the ``use_reducer`` hook, instead of
directly assigning a new state, allows you to specify an action which will transition
the previous state into the next state. This transition is defined by a reducer function
of the form ``(current_state, action) -> new_state``. The ``use_reducer`` hook then
returns the current state and a ``dispatch_action`` function that accepts an ``action``
and causes a transition to the next state via the ``reducer``.

``use_reducer`` is generally preferred to ``use_state`` if logic for transitioning from
one state to the next is especially complex or involves nested data structures.
``use_reducer`` can also be used to collect several ``use_state`` calls together - this
may be slightly more performant as well as being preferable since there is only one
``dispatch_action`` callback versus the many ``set_state`` callbacks.

We can rework the :ref:`Functional Updates` counter example to use ``use_reducer``:

.. reactpy:: _examples/use_reducer_counter

.. note::

    The identity of the ``dispatch_action`` function is guaranteed to be preserved
    across re-renders throughout the lifetime of the component. This means it can safely
    be omitted from dependency lists in :ref:`Use Effect` or :ref:`Use Callback`.


Use Callback
------------

.. code-block::

    memoized_callback = use_callback(lambda: do_something(a, b))

A derivative of :ref:`Use Memo`, the ``use_callback`` hook returns a
`memoized <memoization>`_ callback. This is useful when passing callbacks to child
components which check reference equality to prevent unnecessary renders. The
``memoized_callback`` will only change when any local variables is references do.

.. note::

    You may manually specify what values the callback depends on in the :ref:`same way
    as effects <Manual Effect Conditions>` using the ``dependencies`` parameter.


Use Memo
--------

.. code-block::

    memoized_value = use_memo(lambda: compute_something_expensive(a, b))

Returns a `memoized <memoization>`_ value. By passing a constructor function accepting
no arguments and an array of dependencies for that constructor, the ``use_callback``
hook will return the value computed by the constructor. The ``memoized_value`` will only
be recomputed if a local variable referenced by the constructor changes (e.g. ``a`` or
``b`` here). This optimizes performance because you don't need to
``compute_something_expensive`` on every render.

Unlike ``use_effect`` the constructor function is called during each render (instead of
after) and should not incur side effects.

.. warning::

    Remember that you shouldn't optimize something unless you know it's a performance
    bottleneck. Write your code without ``use_memo`` first and then add it to targeted
    sections that need a speed-up.

.. note::

    You may manually specify what values the callback depends on in the :ref:`same way
    as effects <Manual Effect Conditions>` using the ``dependencies`` parameter.


Use Ref
-------

.. code-block::

    ref_container = use_ref(initial_value)

Returns a mutable :class:`~reactpy.utils.Ref` object that has a single
:attr:`~reactpy.utils.Ref.current` attribute that at first contains the ``initial_state``.
The identity of the ``Ref`` object will be preserved for the lifetime of the component.

A ``Ref`` is most useful if you need to incur side effects since updating its
``.current`` attribute doesn't trigger a re-render of the component. You'll often use this
hook alongside :ref:`Use Effect` or in response to component event handlers.


.. links
.. =====

.. _React Hooks: https://reactjs.org/docs/hooks-reference.html
.. _side effects: https://en.wikipedia.org/wiki/Side_effect_(computer_science)
.. _memoization: https://en.wikipedia.org/wiki/Memoization


Rules of Hooks
==============

Hooks are just normal Python functions, but there's a bit of magic to them, and in order
for that magic to work you've got to follow two rules. Thankfully we supply a
:ref:`Flake8 Plugin` to help enforce them.


Only call outside flow controls
-------------------------------

**Don't call hooks inside loops, conditions, or nested functions.** Instead you must
always call hooks at the top level of your functions. By adhering to this rule you
ensure that hooks are always called in the exact same order. This fact is what allows
ReactPy to preserve the state of hooks between multiple calls to ``useState`` and
``useEffect`` calls.


Only call in render functions
-----------------------------

**Don't call hooks from regular Python functions.** Instead you should:

- ✅ Call Hooks from a component's render function.

- ✅ Call Hooks from another custom hook

Following this rule ensures stateful logic for ReactPy component is always clearly
separated from the rest of your codebase.


Flake8 Plugin
-------------

We provide a Flake8 plugin called `flake8-reactpy-hooks <Flake8 Linter Plugin>`_ that helps
to enforce the two rules described above. You can ``pip`` install it directly, or with
the ``lint`` extra for ReactPy:

.. code-block:: bash

    pip install flake8-reactpy-hooks

Once installed running, ``flake8`` on your code will start catching errors. For example:

.. code-block:: bash

    flake8 my_reactpy_components.py

Might produce something like the following output:

.. code-block:: text

    ./my_reactpy_components:10:8 ROH102 hook 'use_effect' used inside if statement
    ./my_reactpy_components:23:4 ROH102 hook 'use_state' used outside component or hook definition

See the Flake8 docs for
`more info <https://flake8.pycqa.org/en/latest/user/configuration.html>`__.

.. links
.. =====

.. _Flake8 Linter Plugin: https://github.com/reactive-python/flake8-reactpy-hooks

docs/source/reference/browser-events.rst METASEP
.. _Browser Events:

Browser Events 🚧
=================

The event types below are triggered by an event in the bubbling phase. To register an
event handler for the capture phase, append Capture to the event name; for example,
instead of using ``onClick``, you would use ``onClickCapture`` to handle the click event
in the capture phase.

.. note::

    Under construction 🚧


Clipboard Events
----------------

Composition Events
------------------

Keyboard Events
---------------

Focus Events
------------

Form Events
-----------

Generic Events
--------------

Mouse Events
------------

Pointer Events
--------------

Selection Events
----------------

Touch Events
------------

UI Events
---------

Wheel Events
------------

Media Events
------------

Image Events
------------

Animation Events
----------------

Transition Events
-----------------

Other Events
------------

docs/source/about/contributor-guide.rst METASEP
Contributor Guide
=================

.. note::

    The
    `Code of Conduct <https://github.com/reactive-python/reactpy/blob/main/CODE_OF_CONDUCT.md>`__
    applies in all community spaces. If you are not familiar with our Code of Conduct
    policy, take a minute to read it before making your first contribution.

The ReactPy team welcomes contributions and contributors of all kinds - whether they come
as code changes, participation in the discussions, opening issues and pointing out bugs,
or simply sharing your work with your colleagues and friends. We're excited to see how
you can help move this project and community forward!


.. _everyone can contribute:

Everyone Can Contribute!
------------------------

Trust us, there's so many ways to support the project. We're always looking for people
who can:

- Improve our documentation
- Teach and tell others about ReactPy
- Share ideas for new features
- Report bugs
- Participate in general discussions

Still aren't sure what you have to offer? Just :discussion-type:`ask us <question>` and
we'll help you make your first contribution.


Making a Pull Request
---------------------

To make your first code contribution to ReactPy, you'll need to install Git_ (or
`Git Bash`_ on Windows). Thankfully there are many helpful
`tutorials <https://github.com/firstcontributions/first-contributions/blob/master/README.md>`__
about how to get started. To make a change to ReactPy you'll do the following:

`Fork ReactPy <https://docs.github.com/en/github/getting-started-with-github/fork-a-repo>`__:
    Go to `this URL <https://github.com/reactive-python/reactpy>`__ and click the "Fork" button.

`Clone your fork <https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository>`__:
    You use a ``git clone`` command to copy the code from GitHub to your computer.

`Create a new branch <https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging>`__:
    You'll ``git checkout -b your-first-branch`` to create a new space to start your work.

:ref:`Prepare your Development Environment <Development Environment>`:
    We explain in more detail below how to install all ReactPy's dependencies.

`Push your changes <https://docs.github.com/en/github/using-git/pushing-commits-to-a-remote-repository>`__:
    Once you've made changes to ReactPy, you'll ``git push`` them to your fork.

:ref:`Create a changelog entry <Creating a changelog entry>`:
    Record your changes in the :ref:`changelog` so we can publicize them in the next release.

`Create a Pull Request <https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request>`__:
    We'll review your changes, run some :ref:`tests <Running The Tests>` and
    :ref:`equality checks <Code Quality Checks>` and, with any luck, accept your request.
    At that point your contribution will be merged into the main codebase!


Development Environment
-----------------------

.. note::

    If you have any questions during set up or development post on our
    :discussion-type:`discussion board <question>` and we'll answer them.

In order to develop ReactPy locally you'll first need to install the following:

.. list-table::
    :header-rows: 1

    *   - What to Install
        - How to Install

    *   - Git
        - https://git-scm.com/book/en/v2/Getting-Started-Installing-Git

    *   - Python >= 3.7
        - https://realpython.com/installing-python/

    *   - NodeJS >= 14
        - https://nodejs.org/en/download/package-manager/

    *   - NPM >= 7.13
        - https://docs.npmjs.com/try-the-latest-stable-version-of-npm

    *   - Docker (optional)
        - https://docs.docker.com/get-docker/

.. note::

    NodeJS distributes a version of NPM, but you'll want to get the latest

Once done, you can clone a local copy of this repository:

.. code-block:: bash

    git clone https://github.com/reactive-python/reactpy.git
    cd reactpy

Then, you should be able to run the command below to:

- Install an editable version of the Python code

- Download, build, and install Javascript dependencies

- Install some pre-commit_ hooks for Git

.. code-block:: bash

    pip install -e . -r requirements.txt && pre-commit install

If you modify any Javascript, you'll need to re-install ReactPy:

.. code-block:: bash

    pip install -e .

However you may also ``cd`` to the ``src/client`` directory which contains a
``package.json`` that you can use to run standard ``npm`` commands from.


Running The Tests
-----------------

The test suite for ReactPy is executed with Nox_, which should already be installed if you
followed the `earlier instructions <Development Environment>`_. The suite covers:

1. Server-side Python code with PyTest_

2. The end-to-end application using Playwright_ in Python

3. Client-side Javascript code with UVU_

Once you've installed them you'll be able to run:

.. code-block:: bash

    nox -s check-python-tests

You can observe the browser as the tests are running by passing an extra flag:

.. code-block:: bash

    nox -s check-python-tests -- --headed

To see a full list of available commands (e.g. ``nox -s <your-command>``) run:

.. code-block:: bash

    nox -l


Code Quality Checks
-------------------

Several tools are run on the codebase to help validate its quality. For the most part,
if you set up your :ref:`Development Environment` with pre-commit_ to check your work
before you commit it, then you'll be notified when changes need to be made or, in the
best case, changes will be made automatically for you.

The following are currently being used:

- MyPy_ - a static type checker
- Black_ - an opinionated code formatter
- Flake8_ - a style guide enforcement tool
- ISort_ - a utility for alphabetically sorting imports
- Prettier_ - a tool for automatically formatting various file types

The most strict measure of quality enforced on the codebase is 100% test coverage in
Python files. This means that every line of coded added to ReactPy requires a test case
that exercises it. This doesn't prevent all bugs, but it should ensure that we catch the
most common ones.

If you need help understanding why code you've submitted does not pass these checks,
then be sure to ask, either in the :discussion-type:`Community Forum <question>` or in
your :ref:`Pull Request <Making a Pull Request>`.

.. note::

    You can manually run ``nox -s format`` to auto format your code without having to
    do so via ``pre-commit``. However, many IDEs have ways to automatically format upon
    saving a file
    (e.g.`VSCode <https://code.visualstudio.com/docs/python/editing#_formatting>`__)


Building The Documentation
--------------------------

To build and display the documentation locally run:

.. code-block:: bash

    nox -s docs

This will compile the documentation from its source files into HTML, start a web server,
and open a browser to display the now generated documentation. Whenever you change any
source files the web server will automatically rebuild the documentation and refresh the
page. Under the hood this is using
`sphinx-autobuild <https://github.com/executablebooks/sphinx-autobuild>`__.

To run some of the examples in the documentation as if they were tests run:

.. code-block:: bash

    nox -s test_docs

Building the documentation as it's deployed in production requires Docker_. Once you've
installed Docker, you can run:

.. code-block:: bash

    nox -s docs_in_docker

Where you can then navigate to http://localhost:5000..


Creating a Changelog Entry
--------------------------

As part of your pull request, you'll want to edit the `Changelog
<https://github.com/reactive-python/reactpy/blob/main/docs/source/about/changelog.rst>`__ by
adding an entry describing what you've changed or improved. You should write an entry in
the style of `Keep a Changelog <https://keepachangelog.com/>`__ that falls under one of
the following categories, and add it to the :ref:`Unreleased` section of the changelog:

- **Added** - for new features.
- **Changed** - for changes in existing functionality.
- **Deprecated** - for soon-to-be removed features.
- **Removed** - for now removed features.
- **Fixed** - for any bug fixes.
- **Documented** - for improvements to this documentation.
- **Security** - in case of vulnerabilities.

If one of the sections doesn't exist, add it. If it does already, add a bullet point
under the relevant section. Your description should begin with a reference to the
relevant issue or pull request number. Here's a short example of what an unreleased
changelog entry might look like:

.. code-block:: rst

    Unreleased
    ----------

    **Added**

    - :pull:`123` - A really cool new feature

    **Changed**

    - :pull:`456` - The behavior of some existing feature

    **Fixed**

    - :issue:`789` - Some really bad bug

.. hint::

    ``:issue:`` and ``:pull:`` refer to issue and pull request ticket numbers.


Release Process
---------------

Creating a release for ReactPy involves two steps:

1. Tagging a version
2. Publishing a release

To **tag a version** you'll run the following command:

.. code-block:: bash

    nox -s tag -- <the-new-version>

Which will update the version for:

- Python packages
- Javascript packages
- The changelog

You'll be then prompted to confirm the auto-generated updates before those changes will
be staged, committed, and pushed along with a new tag matching ``<the-new-version>``
which was specified earlier.

Lastly, to **publish a release** `create one in GitHub
<https://docs.github.com/en/github/administering-a-repository/releasing-projects-on-github/managing-releases-in-a-repository>`__.
Because we pushed a tag using the command above, there should already be a saved tag you
can target when authoring the release. The release needs a title and description. The
title should simply be the version (same as the tag), and the description should simply
use GitHub's "Auto-generated release notes".


Other Core Repositories
-----------------------

ReactPy depends on, or is used by several other core projects. For documentation on them
you should refer to their respective documentation in the links below:

- `reactpy-js-component-template
  <https://github.com/reactive-python/reactpy-js-component-template>`__ - Template repo
  for making :ref:`Custom Javascript Components`.
- `reactpy-flake8 <https://github.com/reactive-python/reactpy-flake8>`__ - Enforces the
  :ref:`Rules of Hooks`
- `reactpy-jupyter <https://github.com/reactive-python/reactpy-jupyter>`__ - ReactPy integration for
  Jupyter
- `reactpy-dash <https://github.com/reactive-python/reactpy-dash>`__ - ReactPy integration for Plotly
  Dash
- `django-reactpy <https://github.com/reactive-python/django-reactpy>`__ - ReactPy integration for
  Django

.. Links
.. =====

.. _Google Chrome: https://www.google.com/chrome/
.. _Docker: https://docs.docker.com/get-docker/
.. _Git: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git
.. _Git Bash: https://gitforwindows.org/
.. _NPM: https://www.npmjs.com/get-npm
.. _PyPI: https://pypi.org/project/reactpy
.. _pip: https://pypi.org/project/pip/
.. _PyTest: pytest <https://docs.pytest.org
.. _Playwright: https://playwright.dev/python/
.. _Nox: https://nox.thea.codes/en/stable/#
.. _React: https://reactjs.org/
.. _Heroku: https://www.heroku.com/what
.. _GitHub Actions: https://github.com/features/actions
.. _pre-commit: https://pre-commit.com/
.. _GitHub Flow: https://guides.github.com/introduction/flow/
.. _MyPy: http://mypy-lang.org/
.. _Black: https://github.com/psf/black
.. _Flake8: https://flake8.pycqa.org/en/latest/
.. _ISort: https://pycqa.github.io/isort/
.. _UVU: https://github.com/lukeed/uvu
.. _Prettier: https://prettier.io/

docs/source/about/changelog.rst METASEP
Changelog
=========

.. note::

    The ReactPy team manages their short and long term plans with `GitHub Projects
    <https://github.com/orgs/reactive-python/projects/1>`__. If you have questions about what
    the team are working on, or have feedback on how issues should be prioritized, feel
    free to :discussion-type:`open up a discussion <question>`.

All notable changes to this project will be recorded in this document. The style of
which is based on `Keep a Changelog <https://keepachangelog.com/>`__. The versioning
scheme for the project adheres to `Semantic Versioning <https://semver.org/>`__. For
more info, see the :ref:`Contributor Guide <Creating a Changelog Entry>`.


.. INSTRUCTIONS FOR CHANGELOG CONTRIBUTORS
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. If you're adding a changelog entry, be sure to read the "Creating a Changelog Entry"
.. section of the documentation before doing so for instructions on how to adhere to the
.. "Keep a Changelog" style guide (https://keepachangelog.com).

Unreleased
----------

No changes.


v1.0.0
------
:octicon:`milestone` *released on 2023-03-14*

No changes.


v1.0.0-a6
---------
:octicon:`milestone` *released on 2023-02-23*

**Fixed**

- :pull:`936` - remaining issues from :pull:`934`


v1.0.0-a5
---------
:octicon:`milestone` *released on 2023-02-21*

**Fixed**

- :pull:`934` - minor issues with camelCase rewrite CLI utility


v1.0.0-a4
---------
:octicon:`milestone` *released on 2023-02-21*

**Changed**

- :pull:`919` - Reverts :pull:`841` as per the conclusion in :discussion:`916`. but
  preserves the ability to declare attributes with snake_case.

**Deprecated**

- :pull:`919` - Declaration of keys via keywork arguments in standard elements. A script
  has been added to automatically convert old usages where possible.


v1.0.0-a3
---------
:octicon:`milestone` *released on 2023-02-02*

**Fixed**

- :pull:`908` - minor type hint issue with ``VdomDictConstructor``

**Removed**

- :pull:`907` - accidental import of reactpy.testing


v1.0.0-a2
---------
:octicon:`milestone` *released on 2023-01-31*

**Reverted**

- :pull:`901` - reverts :pull:`886` due to :issue:`896`

**Fixed**

- :issue:`896` - Stale event handlers after disconnect/reconnect cycle
- :issue:`898` - Fixed CLI not registered as entry point


v1.0.0-a1
---------
:octicon:`milestone` *released on 2023-01-28*

**Changed**

- :pull:`841` - Revamped element constructor interface. Now instead of passing a
  dictionary of attributes to element constructors, attributes are declared using
  keyword arguments. For example, instead of writing:

  .. code-block::

      html.div({"className": "some-class"}, "some", "text")

  You now should write:

  .. code-block::

      html.div("some", "text", class_name="some-class")

  .. note::

    All attributes are written using ``snake_case``.

  In conjunction, with these changes, ReactPy now supplies a command line utility that
  makes a "best effort" attempt to automatically convert code to the new API. Usage of
  this utility is as follows:

  .. code-block:: bash

      reactpy update-html-usages [PATHS]

  Where ``[PATHS]`` is any number of directories or files that should be rewritten.

  .. warning::

    After running this utility, code comments and formatting may have been altered. It's
    recommended that you run a code formatting tool like `Black
    <https://github.com/psf/black>`__ and manually review and replace any comments that
    may have been moved.

**Fixed**

- :issue:`755` - unification of component and VDOM constructor interfaces. See above.


v0.44.0
-------
:octicon:`milestone` *released on 2023-01-27*

**Deprecated**

- :pull:`876` - ``reactpy.widgets.hotswap``. The function has no clear uses outside of some
  internal applications. For this reason it has been deprecated.

**Removed**

- :pull:`886` - Ability to access element value from events via `event['value']` key.
  Instead element value should be accessed via `event['target']['value']`. Originally
  deprecated in :ref:`v0.34.0`.
- :pull:`886` - old misspelled option ``reactpy.config.REACTPY_WED_MODULES_DIR``. Originally
  deprecated in :ref:`v0.36.1`.


v0.43.0
-------
:octicon:`milestone` *released on 2023-01-09*

**Deprecated**

- :pull:`870` - ``ComponentType.should_render()``. This method was implemented based on
  reading the React/Preact source code. As it turns out though it seems like it's mostly
  a vestige from the fact that both these libraries still support class-based
  components. The ability for components to not render also caused several bugs.

**Fixed**

- :issue:`846` - Nested context does no update value if outer context should not render.
- :issue:`847` - Detached model state on render of context consumer if unmounted and
  context value does not change.


v0.42.0
-------
:octicon:`milestone` *released on 2022-12-02*

**Added**

- :pull:`835` - Ability to customize the ``<head>`` element of ReactPy's built-in client.
- :pull:`835` - ``vdom_to_html`` utility function.
- :pull:`843` - Ability to subscribe to changes that are made to mutable options.
- :pull:`832` - ``del_html_head_body_transform`` to remove ``<html>``, ``<head>``, and ``<body>`` while preserving children.
- :pull:`699` - Support for form element serialization

**Fixed**

- :issue:`582` - ``REACTPY_DEBUG_MODE`` is now mutable and can be changed at runtime
- :pull:`832` - Fix ``html_to_vdom`` improperly removing ``<html>``, ``<head>``, and ``<body>`` nodes.

**Removed**

- :pull:`832` - Removed ``reactpy.html.body`` as it is currently unusable due to technological limitations, and thus not needed.
- :pull:`840` - remove ``REACTPY_FEATURE_INDEX_AS_DEFAULT_KEY`` option
- :pull:`835` - ``serve_static_files`` option from backend configuration

**Deprecated**

- :commit:`8f3785b` - Deprecated ``module_from_template``

v0.41.0
-------
:octicon:`milestone` *released on 2022-11-01*

**Changed**

- :pull:`823` - The hooks ``use_location`` and ``use_scope`` are no longer
  implementation specific and are now available as top-level imports. Instead of each
  backend defining these hooks, backends establish a ``ConnectionContext`` with this
  information.
- :pull:`824` - ReactPy's built-in backend server now expose the following routes:

  - ``/_reactpy/assets/<file-path>``
  - ``/_reactpy/stream/<path>``
  - ``/_reactpy/modules/<file-path>``
  - ``/<prefix>/<path>``

  This should allow the browser to cache static resources. Even if your ``url_prefix``
  is ``/_reactpy``, your app should still work as expected. Though if you're using
  ``reactpy-router``, ReactPy's server routes will always take priority.
- :pull:`824` - Backend implementations now strip any URL prefix in the pathname for
  ``use_location``.
- :pull:`827` - ``use_state`` now returns a named tuple with ``value`` and ``set_value``
  fields. This is convenient for adding type annotations if the initial state value is
  not the same as the values you might pass to the state setter. Where previously you
  might have to do something like:

  .. code-block::

      value: int | None = None
      value, set_value = use_state(value)

  Now you can annotate your state using the ``State`` class:

  .. code-block::

      state: State[int | None] = use_state(None)

      # access value and setter
      state.value
      state.set_value

      # can still destructure if you need to
      value, set_value = state

**Added**

- :pull:`823` - There is a new ``use_connection`` hook which returns a ``Connection``
  object. This ``Connection`` object contains a ``location`` and ``scope``, along with
  a ``carrier`` which is unique to each backend implementation.


v0.40.2
-------
:octicon:`milestone` *released on 2022-09-13*

**Changed**

- :pull:`809` - Avoid the use of JSON patch for diffing models.


v0.40.1
-------
:octicon:`milestone` *released on 2022-09-11*

**Fixed**

- :issue:`806` - Child models after a component fail to render


v0.40.0 (yanked)
----------------
:octicon:`milestone` *released on 2022-08-13*

**Fixed**

- :issue:`777` - Fix edge cases where ``html_to_vdom`` can fail to convert HTML
- :issue:`789` - Conditionally rendered components cannot use contexts
- :issue:`773` - Use strict equality check for text, numeric, and binary types in hooks
- :issue:`801` - Accidental mutation of old model causes invalid JSON Patch

**Changed**

- :pull:`123` - set default timeout on playwright page for testing
- :pull:`787` - Track contexts in hooks as state
- :pull:`787` - remove non-standard ``name`` argument from ``create_context``

**Added**

- :pull:`123` - ``asgiref`` as a dependency
- :pull:`795` - ``lxml`` as a dependency


v0.39.0
-------
:octicon:`milestone` *released on 2022-06-20*

**Fixed**

- :pull:`763` - ``No module named 'reactpy.server'`` from ``reactpy.run``
- :pull:`749` - Setting appropriate MIME type for web modules in `sanic` server implementation

**Changed**

- :pull:`763` - renamed various:

  - ``reactpy.testing.server -> reactpy.testing.backend``
  - ``ServerFixture -> BackendFixture``
  - ``DisplayFixture.server -> DisplayFixture.backend``

- :pull:`765` - ``exports_default`` parameter is removed from ``module_from_template``.

**Added**

- :pull:`765` - ability to specify versions with module templates (e.g.
  ``module_from_template("react@^17.0.0", ...)``).


v0.38.1
-------
:octicon:`milestone` *released on 2022-04-15*

**Fixed**

- `reactive-python/reactpy-jupyter#22 <https://github.com/reactive-python/reactpy-jupyter/issues/22>`__ -
  a missing file extension was causing a problem with WebPack.


v0.38.0
-------
:octicon:`milestone` *released on 2022-04-15*

No changes.


v0.38.0-a4
----------
:octicon:`milestone` *released on 2022-04-15*

**Added**

- :pull:`733` - ``use_debug_value`` hook

**Changed**

- :pull:`733` - renamed ``assert_reactpy_logged`` testing util to ``assert_reactpy_did_log``


v0.38.0-a3
----------
:octicon:`milestone` *released on 2022-04-15*

**Changed**

- :pull:`730` - Layout context management is not async


v0.38.0-a2
----------
:octicon:`milestone` *released on 2022-04-14*

**Added**

- :pull:`721` - Implement ``use_location()`` hook. Navigating to any route below the
  root of the application will be reflected in the ``location.pathname``. This operates
  in concert with how ReactPy's configured routes have changed. This will ultimately work
  towards resolving :issue:`569`.

**Changed**

- :pull:`721` - The routes ReactPy configures on apps have changed

  .. code-block:: text

      prefix/_api/modules/*    web modules
      prefix/_api/stream       websocket endpoint
      prefix/*                 client react app

  This means that ReactPy's client app is available at any route below the configured
  ``url_prefix`` besides ``prefix/_api``. The ``_api`` route will likely remain a route
  which is reserved by ReactPy. The route navigated to below the ``prefix`` will be shown
  in ``use_location``.

- :pull:`721` - ReactPy's client now uses Preact instead of React

- :pull:`726` - Renamed ``reactpy.server`` to ``reactpy.backend``. Other references to "server
  implementations" have been renamed to "backend implementations" throughout the
  documentation and code.

**Removed**

- :pull:`721` - ``redirect_root`` server option


v0.38.0-a1
----------
:octicon:`milestone` *released on 2022-03-27*

**Changed**

- :pull:`703` - How ReactPy integrates with servers. ``reactpy.run`` no longer accepts an app
  instance to discourage use outside of testing. ReactPy's server implementations now
  provide ``configure()`` functions instead. ``reactpy.testing`` has been completely
  reworked in order to support async web drivers
- :pull:`703` - ``PerClientStateServer`` has been functionally replaced by ``configure``

**Added**

- :issue:`669` - Access to underlying server requests via contexts

**Removed**

- :issue:`669` - Removed ``reactpy.widgets.multiview`` since basic routing view ``use_scope`` is
  now possible as well as all ``SharedClientStateServer`` implementations.

**Fixed**

- :issue:`591` - ReactPy's test suite no longer uses sync web drivers
- :issue:`678` - Updated Sanic requirement to ``>=21``
- :issue:`657` - How we advertise ``reactpy.run``


v0.37.2
-------
:octicon:`milestone` *released on 2022-03-27*

**Changed**

- :pull:`701` - The name of ``proto`` modules to ``types`` and added a top level
  ``reactpy.types`` module

**Fixed**

- :pull:`716` - A typo caused ReactPy to use the insecure ``ws`` web-socket protocol on
  pages loaded with ``https`` instead of the secure ``wss`` protocol


v0.37.1
-------
:octicon:`milestone` *released on 2022-03-05*

No changes.


v0.37.1-a2
----------
:octicon:`milestone` *released on 2022-03-02*

**Fixed:**

- :issue:`684` - Revert :pull:`694` and by making ``value`` uncontrolled client-side


v0.37.1-a1
----------
:octicon:`milestone` *released on 2022-02-28*

**Fixed:**

- :issue:`684` - ``onChange`` event for inputs missing key strokes


v0.37.0
-------
:octicon:`milestone` *released on 2022-02-27*

**Added:**

- :issue:`682` - Support for keys in HTML fragments
- :pull:`585` - Use Context Hook

**Fixed:**

- :issue:`690` - React warning about set state in unmounted component
- :pull:`688` - Missing reset of schedule_render_later flag

----

Releases below do not use the "Keep a Changelog" style guidelines.

----

v0.36.3
-------
:octicon:`milestone` *released on 2022-02-18*

Misc bug fixes along with a minor improvement that allows components to return ``None``
to render nothing.

**Closed Issues**

- All child states wiped upon any child key change - :issue:`652`
- Allow NoneType returns within components - :issue:`538`

**Merged Pull Requests**

- fix #652 - :pull:`672`
- Fix 663 - :pull:`667`


v0.36.2
-------
:octicon:`milestone` *released on 2022-02-02*

Hot fix for newly introduced ``DeprecatedOption``:

- :commit:`c146dfb264cbc3d2256a62efdfe9ccf62c795b01`


v0.36.1
-------
:octicon:`milestone` *released on 2022-02-02*

Includes bug fixes and renames the configuration option ``REACTPY_WED_MODULES_DIR`` to
``REACTPY_WEB_MODULES_DIR`` with a corresponding deprecation warning.

**Closed Issues**

- Fix Key Error When Cleaning Up Event Handlers - :issue:`640`
- Update Script Tag Behavior - :issue:`628`

**Merged Pull Requests**

- mark old state as None if unmounting - :pull:`641`
- rename REACTPY_WED_MODULES_DIR to REACTPY_WEB_MODULES_DIR - :pull:`638`


v0.36.0
-------
:octicon:`milestone` *released on 2022-01-30*

This release includes an important fix for errors produced after :pull:`623` was merged.
In addition there is not a new ``http.script`` element which can behave similarly to a
standard HTML ``<script>`` or, if no attributes are given, operate similarly to an
effect. If no attributes are given, and when the script evaluates to a function, that
function will be called the first time it is mounted and any time the content of the
script is subsequently changed. If the function then returns another function, that
returned function will be called when the script is removed from the view, or just
before the content of the script changes.

**Closed Issues**

- State mismatch during component update - :issue:`629`
- Implement a script tag - :issue:`544`

**Pull Requests**

- make scripts behave more like normal html script element - :pull:`632`
- Fix state mismatch during component update - :pull:`631`
- implement script element - :pull:`617`


v0.35.4
-------
:octicon:`milestone` *released on 2022-01-27*

Keys for elements at the root of a component were not being tracked. Thus key changes
for elements at the root did not trigger unmounts.

**Closed Issues**

- Change Key of Parent Element Does Not Unmount Children - :issue:`622`

**Pull Requests**

- fix issue with key-based identity - :pull:`623`


v0.35.3
-------
:octicon:`milestone` *released on 2022-01-27*

As part of :pull:`614`, elements which changed type were not deeply unmounted. This
behavior is probably undesirable though since the state for children of the element
in question would persist (probably unexpectedly).

**Pull Requests**

- Always deeply unmount - :pull:`620`


v0.35.2
-------
:octicon:`milestone` *released on 2022-01-26*

This release includes several bug fixes. The most significant of which is the ability to
change the type of an element in the try (i.e. to and from being a component) without
getting an error. Originally the errors were introduced because it was though changing
element type would not be desireable. This was not the case though - swapping types
turns out to be quite common and useful.

**Closed Issues**

- Allow Children with the Same Key to Vary in Type - :issue:`613`
- Client Always Looks for Server at "/"  - :issue:`611`
- Web modules get double file extensions with v0.35.x - :issue:`605`

**Pull Requests**

- allow elements with the same key to change type - :pull:`614`
- make connection to websocket relative path - :pull:`612`
- fix double file extension - :pull:`606`


v0.35.1
-------
:octicon:`milestone` *released on 2022-01-18*

Re-add accidentally deleted ``py.typed`` file to distribution. See `PEP-561
<https://www.python.org/dev/peps/pep-0561/#packaging-type-information>`__ for info on
this marker file.


v0.35.0
-------
:octicon:`milestone` *released on 2022-01-18*

The highlight of this release is that the default :ref:`"key" <Organizing Items With
Keys>` of all elements will be their index amongst their neighbors. Previously this
behavior could be engaged by setting ``REACTPY_FEATURE_INDEX_AS_DEFAULT_KEY=1`` when
running ReactPy. In this release though, you will need to explicitly turn off this feature
(i.e. ``=0``) to return to the old behavior. With this change, some may notice
additional error logs which warn that:

.. code-block:: text

  Key not specified for child in list ...

This is saying is that an element or component which was created in a list does not have
a unique ``key``. For more information on how to mitigate this warning refer to the docs
on :ref:`Organizing Items With Keys`.

**Closed Issues**

- Support Starlette Server - :issue:`588`
- Fix unhandled case in module_from_template - :issue:`584`
- Hide "Children" within REACTPY_DEBUG_MODE key warnings - :issue:`562`
- Bug in Element Key Identity - :issue:`556`
- Add iFrame to reactpy.html - :issue:`542`
- Create a use_linked_inputs widget instead of Input - :issue:`475`
- React warning from module_from_template - :issue:`440`
- Use Index as Default Key - :issue:`351`

**Pull Requests**

- add ``use_linked_inputs`` - :pull:`593`
- add starlette server implementation - :pull:`590`
- Log on web module replacement instead of error - :pull:`586`
- Make Index Default Key - :pull:`579`
- reduce log spam from missing keys in children - :pull:`564`
- fix bug in element key identity - :pull:`563`
- add more standard html elements - :pull:`554`


v0.34.0
-------
:octicon:`milestone` *released on 2021-12-16*

This release contains a variety of minor fixes and improvements which came out of
rewriting the documentation. The most significant of these changes is the remove of
target element attributes from the top-level of event data dictionaries. For example,
instead of being able to find the value of an input at ``event["value"]`` it will
instead be found at ``event["target"]["value"]``. For a short period we will issue a
:class:`DeprecationWarning` when target attributes are requested at the top-level of the
event dictionary. As part of this change we also add ``event["currentTarget"]`` and
``event["relatedTarget"]`` keys to the event dictionary as well as a
``event[some_target]["boundingClientRect"]`` where ``some_target`` may be ``"target"``,
``"currentTarget"`` or ``"relatedTarget"``.

**Closed Issues**

- Move target attributes to ``event['target']`` - :issue:`548`

**Pull Requests**

- Correctly Handle Target Event Data - :pull:`550`
- Clean up WS console logging - :pull:`522`
- automatically infer closure arguments - :pull:`520`
- Documentation Rewrite - :pull:`519`
- add option to replace existing when creating a module - :pull:`516`


v0.33.3
-------
:octicon:`milestone` *released on 2021-10-08*

Contains a small number of bug fixes and improvements. The most significant change is
the addition of a warning stating that `REACTPY_FEATURE_INDEX_AS_DEFAULT_KEY=1` will become
the default in a future release. Beyond that, a lesser improvement makes it possible to
use the default export from a Javascript module when calling `module_from_template` by
specifying `exports_default=True` as a parameter. A

**Closed Issues**

- Memory leak in SharedClientStateServer - :issue:`511`
- Cannot use default export in react template - :issue:`502`
- Add warning that element index will be used as the default key in a future release - :issue:`428`

**Pull Requests**

- warn that REACTPY_FEATURE_INDEX_AS_DEFAULT_KEY=1 will be the default - :pull:`515`
- clean up patch queues after exit - :pull:`514`
- Remove Reconnecting WS alert - :pull:`513`
- Fix 502 - :pull:`503`


v0.33.2
-------
:octicon:`milestone` *released on 2021-09-05*

A release to fix a memory leak caused by event handlers that were not being removed
when components updated.

**Closed Issues**

- Non-root component event handlers cause memory leaks - :issue:`510`


v0.33.1
-------
:octicon:`milestone` *released on 2021-09-02*

A hot fix for a regression introduced in ``0.33.0`` where the root element of the layout
could not be updated. See :issue:`498` for more info. A regression test for this will
be introduced in a future release.

**Pull Requests**

- Fix 498 pt1 - :pull:`501`


v0.33.0
-------
:octicon:`milestone` *released on 2021-09-02*

The most significant fix in this release is for a regression which manifested in
:issue:`480`, :issue:`489`, and :issue:`451` which resulted from an issue in the way
JSON patches were being applied client-side. This was ultimately resolved by
:pull:`490`. While it's difficult to test this without a more thorough Javascript
suite, we added a test that should hopefully catch this in the future by proxy.

The most important breaking change, is yet another which modifies the Custom Javascript
Component interface. We now add a ``create()`` function to the ``bind()`` interface that
allows ReactPy's client to recursively create components from that (and only that) import
source. Prior to this, the interface was given unrendered models for child elements. The
imported module was then responsible for rendering them. This placed a large burden on
the author to understand how to handle these unrendered child models. In addition, in
the React template used by ``module_from_template`` we needed to import a version of
``@reactpy/client`` from the CDN - this had already caused some issues where the
template required a version of ``@reactpy/client`` in the which had not been released
yet.

**Closed Issues**

- Client-side error in mount-01d35dc3.js - :issue:`489`
- Style Cannot Be Updated - :issue:`480`
- Displaying error messages in the client via `__error__` tag can leak secrets - :issue:`454`
- Examples broken in docs  - :issue:`451`
- Rework docs landing page - :issue:`446`
- eventHandlers should be a mapping of generic callables - :issue:`423`
- Allow customization of built-in ReactPy client - :issue:`253`

**Pull Requests**

- move VdomDict and VdomJson to proto - :pull:`492`
- only send error info in debug mode - :pull:`491`
- correctly apply client-side JSON patch - :pull:`490`
- add script to set version of all packages in ReactPy - :pull:`483`
- Pass import source to bind - :pull:`482`
- Do not mutate client-side model - :pull:`481`
- assume import source children come from same source - :pull:`479`
- make an EventHandlerType protocol - :pull:`476`
- Update issue form - :pull:`471`


v0.32.0
-------
:octicon:`milestone` *released on 2021-08-20*

In addition to a variety of bug fixes and other minor improvements, there's a breaking
change to the custom component interface - instead of exporting multiple functions that
render custom components, we simply expect a single ``bind()`` function.
binding function then must return an object with a ``render()`` and ``unmount()``
function. This change was made in order to better support the rendering of child models.
See :ref:`Custom JavaScript Components` for details on the new interface.

**Closed Issues**

- Docs broken on Firefox - :issue:`469`
- URL resolution for web modules does not consider urls starting with / - :issue:`460`
- Query params in package name for module_from_template not stripped - :issue:`455`
- Make docs section margins larger - :issue:`450`
- Search broken in docs - :issue:`443`
- Move src/reactpy/client out of Python package - :issue:`429`
- Use composition instead of classes async with Layout and LifeCycleHook  - :issue:`412`
- Remove Python language extension - :issue:`282`
- Add keys to models so React doesn't complain of child arrays requiring them -
  :issue:`255`
- Fix binder link in docs - :issue:`231`

**Pull Requests**

- Update issue form - :pull:`471`
- improve heading legibility - :pull:`470`
- fix search in docs by upgrading sphinx - :pull:`462`
- rework custom component interface with bind() func - :pull:`458`
- parse package as url path in module_from_template - :pull:`456`
- add file extensions to import - :pull:`439`
- fix key warnings - :pull:`438`
- fix #429 - move client JS to top of src/ dir - :pull:`430`


v0.31.0
-------
:octicon:`milestone` *released on 2021-07-14*

The :class:`~reactpy.core.layout.Layout` is now a prototype, and ``Layout.update`` is no
longer a public API. This is combined with a much more significant refactor of the
underlying rendering logic.

The biggest issue that has been resolved relates to the relationship between
:class:`~reactpy.core.hooks.LifeCycleHook` and ``Layout``. Previously, the
``LifeCycleHook`` accepted a layout instance in its constructor and called
``Layout.update``. Additionally, the ``Layout`` would manipulate the
``LifeCycleHook.component`` attribute whenever the component instance changed after a
render. The former behavior leads to a non-linear code path that's a touch to follow.
The latter behavior is the most egregious design issue since there's absolutely no local
indication that the component instance can be swapped out (not even a comment).

The new refactor no longer binds component or layout instances to a ``LifeCycleHook``.
Instead, the hook simply receives an un-parametrized callback that can be triggered to
schedule a render. While some error logs lose clarity (since we can't say what component
caused them). This change precludes a need for the layout to ever mutate the hook.

To accommodate this change, the internal representation of the layout's state had to
change. Previously, a class-based approach was take, where methods of the state-holding
classes were meant to handle all use cases. Now we rely much more heavily on very simple
(and mostly static) data structures that have purpose built constructor functions that
much more narrowly address each use case.

After these refactors, ``ComponentTypes`` no longer needs a unique ``id`` attribute.
Instead, a unique ID is generated internally which is associated with the
``LifeCycleState``, not component instances since they are inherently transient.

**Pull Requests**

- fix #419 and #412 - :pull:`422`


v0.30.1
-------
:octicon:`milestone` *released on 2021-07-13*

Removes the usage of the :func:`id` function for generating unique ideas because there
were situations where the IDs bound to the lifetime of an object are problematic. Also
adds a warning :class:`Deprecation` warning to render functions that include the
parameter ``key``. It's been decided that allowing ``key`` to be used in this way can
lead to confusing bugs.

**Pull Requests**

- warn if key is param of component render function - :pull:`421`
- fix :issue:`417` and :issue:`413` - :pull:`418`
- add changelog entry for :ref:`v0.30.0` - :pull:`415`


v0.30.0
-------
:octicon:`milestone` *released on 2021-06-28*

With recent changes to the custom component interface, it's now possible to remove all
runtime reliance on NPM. Doing so has many virtuous knock-on effects:

1. Removal of large chunks of code
2. Greatly simplifies how users dynamically experiment with React component libraries,
   because their usage no longer requires a build step. Instead they can be loaded in
   the browser from a CDN that distributes ESM modules.
3. The built-in client code needs to make fewer assumption about where static resources
   are located, and as a result, it's also easier to coordinate the server and client
   code.
4. Alternate client implementations benefit from this simplicity. Now, it's possible to
   install @reactpy/client normally and write a ``loadImportSource()`` function that
   looks for route serving the contents of `REACTPY_WEB_MODULES_DIR.`

This change includes large breaking changes:

- The CLI is being removed as it won't be needed any longer
- The `reactpy.client` is being removed in favor of a stripped down ``reactpy.web`` module
- The `REACTPY_CLIENT_BUILD_DIR` config option will no longer exist and a new
  ``REACTPY_WEB_MODULES_DIR`` which only contains dynamically linked web modules. While
  this new directory's location is configurable, it is meant to be transient and should
  not be re-used across sessions.

The new ``reactpy.web`` module takes a simpler approach to constructing import sources and
expands upon the logic for resolving imports by allowing exports from URLs to be
discovered too. Now, that ReactPy isn't using NPM to dynamically install component
libraries ``reactpy.web`` instead creates JS modules from template files and links them
into ``REACTPY_WEB_MODULES_DIR``. These templates ultimately direct the browser to load the
desired library from a CDN.

**Pull Requests**

- Add changelog entry for 0.30.0 - :pull:`415`
- Fix typo in index.rst - :pull:`411`
- Add event handlers docs - :pull:`410`
- Misc doc improvements - :pull:`409`
- Port first ReactPy article to docs - :pull:`408`
- Test build in CI - :pull:`404`
- Remove all runtime reliance on NPM - :pull:`398`


v0.29.0
-------
:octicon:`milestone` *released on 2021-06-20*

Contains breaking changes, the most significant of which are:

- Moves the runtime client build directory to a "user data" directory rather a directory
  where ReactPy's code was installed. This has the advantage of not requiring write
  permissions to rebuild the client if ReactPy was installed globally rather than in a
  virtual environment.
- The custom JS component interface has been reworked to expose an API similar to
  the ``createElement``, ``render``, ``unmountComponentAtNode`` functions from React.

**Issues Fixed:**

- :issue:`375`
- :issue:`394`
- :issue:`401`

**Highlighted Commits:**

- add try/except around event handling - :commit:`f2bf589`
- do not call find_builtin_server_type at import time - :commit:`e29745e`
- import default from react/reactDOM/fast-json-patch - :commit:`74c8a34`
- no named exports for react/reactDOM - :commit:`f13bf35`
- debug logs for runtime build dir create/update - :commit:`af94f4e`
- put runtime build in user data dir - :commit:`0af69d2`
- change shared to update_on_change - :commit:`6c09a86`
- rework js module interface + fix docs - :commit:`699cc66`
- correctly serialize File object - :commit:`a2398dc`


v0.28.0
-------
:octicon:`milestone` *released on 2021-06-01*

Includes a wide variety of improvements:

- support ``currentTime`` attr of audio/video elements
- support for the ``files`` attribute from the target of input elements
- model children are passed to the Javascript ``mount()`` function
- began to add tests to client-side javascript
- add a ``mountLayoutWithWebSocket`` function to ``@reactpy/client``

and breaking changes, the most significant of which are:

- Refactor existing server implementations as functions adhering to a protocol. This
  greatly simplified much of the code responsible for setting up servers and avoids
  the use of inheritance.
- Switch to a monorepo-style structure for Javascript enabling a greater separation of
  concerns and common workspace scripts in ``package.json``.
- Use a ``loadImportSource()`` function instead of trying to infer the path to dynamic
  modules which was brittle and inflexible. Allowing the specific client implementation
  to discover where "import sources" are located means ``@reactpy/client`` doesn't
  need to try and devise a solution that will work for all cases. The fallout from this
  change is the addition of `importSource.sourceType` which, for the moment can either
  be ``"NAME"`` or ``"URL"`` where the former indicates the client is expected to know
  where to find a module of that name, and the latter should (usually) be passed on to
  ``import()``


**Issues Fixed:**

- :issue:`324` (partially resolved)
- :issue:`375`

**Highlighted Commits:**

- xfail due to bug in Python - :commit:`fee49a7`
- add importSource sourceType field - :commit:`795bf94`
- refactor client to use loadImportSource param - :commit:`bb5e3f3`
- turn app into a package - :commit:`b282fc2`
- add debug logs - :commit:`4b4f9b7`
- add basic docs about JS test suite - :commit:`9ecfde5`
- only use nox for python tests - :commit:`5056b7b`
- test event serialization - :commit:`05fd86c`
- serialize files attribute of file input element - :commit:`f0d00b7`
- rename hasMount to exportsMount - :commit:`d55a28f`
- refactor flask - :commit:`94681b6`
- refactor tornado + misc fixes to sanic/fastapi - :commit:`16c9209`
- refactor fastapi using server protocol - :commit:`0cc03ba`
- refactor sanic server - :commit:`43d4b4f`
- use server protocol instead of inheritance - :commit:`abe0fde`
- support currentTime attr of audio/video elements - :commit:`975b54a`
- pass children as props to mount() - :commit:`9494bc0`


v0.27.0
-------
:octicon:`milestone` *released on 2021-05-14*

Introduces changes to the interface for custom Javascript components. This now allows
JS modules to export a ``mount(element, component, props)`` function which can be used
to bind new elements to the DOM instead of using the application's own React instance
and specifying React as a peer dependency. This avoids a wide variety of potential
issues with implementing custom components and opens up the possibility for a wider
variety of component implementations.

**Highlighted Commits:**

- modules with mount func should not have children - :commit:`94d006c`
- limit to flask<2.0 - :commit:`e7c11d0`
- federate modules with mount function - :commit:`bf63a62`


v0.26.0
-------
:octicon:`milestone` *released on 2021-05-07*

A collection of minor fixes and changes that, as a whole, add up to something requiring
a minor release. The most significant addition is a fix for situations where a
``Layout`` can raise an error when a component whose state has been delete is rendered.
This occurs when element has been unmounted, but a latent event tells the layout it
should be updated. For example, when a user clicks a button rapidly, and the resulting
update deletes the original button.

**Highlighted Commits:**

- only one attr dict in vdom constructor - :commit:`555086a`
- remove Option setter/getter with current property - :commit:`2627f79`
- add cli command to show options - :commit:`c9e6869`
- check component has model state before render - :commit:`6a50d56`
- rename daemon to run_in_thread + misc - :commit:`417b687`


v0.25.0
-------
:octicon:`milestone` *released on 2021-04-30*

Completely refactors layout dispatcher by switching from a class-based approach to one
that leverages pure functions. While the logic itself isn't any simpler, it was easier
to implement, and now hopefully understand, correctly. This conversion was motivated by
several bugs that had cropped up related to improper usage of ``anyio``.

**Issues Fixed:**

- :issue:`330`
- :issue:`298`

**Highlighted Commits:**

- improve docs + simplify multi-view - :commit:`4129b60`
- require anyio>=3.0 - :commit:`24aed28`
- refactor dispatchers - :commit:`ce8e060`


v0.24.0
-------
:octicon:`milestone` *released on 2021-04-18*

This release contains an update that allows components and elements to have "identity".
That is, their state can be preserved across updates. Before this point, only the state
for the component at the root of an update was preserved. Now though, the state for any
component and element with a ``key`` that is unique amongst its siblings, will be
preserved so long as this is also true for parent elements/components within the scope
of the current update. Thus, only when the key of the element or component changes will
its state do the same.

In a future update, the default key for all elements and components will be its index
with respect to its siblings in the layout. The
:attr:`~reactpy.config.REACTPY_FEATURE_INDEX_AS_DEFAULT_KEY` feature flag has been introduced
to allow users to enable this behavior early.

**Highlighted Commits:**

- add feature flag for default key behavior - :commit:`42ee01c`
- use unique object instead of index as default key - :commit:`5727ab4`
- make HookCatcher/StaticEventHandlers testing utils - :commit:`1abfd76`
- add element and component identity - :commit:`5548f02`
- minor doc updates - :commit:`e5511d9`
- add tests for callback identity preservation with keys - :commit:`72e03ec`
- add 'key' to VDOM spec - :commit:`c3236fe`
- Rename validate_serialized_vdom to validate_vdom_json - :commit:`d04faf9`
- EventHandler should not serialize itself - :commit:`f7a59f2`
- fix docs typos - :commit:`42b2e20`
- fixes: #331 - add roadmap to docs - :commit:`4226c12`


v0.23.1
-------
:octicon:`milestone` *released on 2021-04-02*

**Highlighted Commits:**

- fix non-deterministic return order in install() - :commit:`494d5c2`


v0.23.0
-------
:octicon:`milestone` *released on 2021-04-01*

**Highlighted Commits:**

- add changelog to docs - :commit:`9cbfe94`
- automatically reconnect to server - :commit:`3477e2b`
- allow no reconnect in client - :commit:`ef263c2`
- cleaner way to specify import sources - :commit:`ea19a07`
- add the reactpy-react-client back into the main repo - :commit:`5dcc3bb`
- implement fastapi render server - :commit:`94e0620`
- improve docstring for REACTPY_CLIENT_BUILD_DIR - :commit:`962d885`
- cli improvements - :commit:`788fd86`
- rename SERIALIZED_VDOM_JSON_SCHEMA to VDOM_JSON_SCHEMA - :commit:`74ad578`
- better logging for modules - :commit:`39565b9`
- move client utils into private module - :commit:`f825e96`
- redirect BUILD_DIR imports to REACTPY_CLIENT_BUILD_DIR option - :commit:`53fb23b`
- upgrade snowpack - :commit:`5697a2d`
- better logs for reactpy.run + flask server - :commit:`2b34e3d`
- move package to src dir - :commit:`066c9c5`
- reactpy restore uses backup - :commit:`773f78e`

docs/source/_static/install-and-run-reactpy.gif METASEP

docs/source/_custom_js/rollup.config.js METASEP
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import replace from "@rollup/plugin-replace";

export default {
  input: "src/index.js",
  output: {
    file: "../_static/custom.js",
    format: "esm",
  },
  plugins: [
    resolve(),
    commonjs(),
    replace({
      "process.env.NODE_ENV": JSON.stringify("production"),
      preventAssignment: true,
    }),
  ],
  onwarn: function (warning) {
    if (warning.code === "THIS_IS_UNDEFINED") {
      // skip warning where `this` is undefined at the top level of a module
      return;
    }
    console.warn(warning.message);
  },
};

docs/source/_custom_js/package.json METASEP
{
  "name": "reactpy-docs-example-loader",
  "version": "1.0.0",
  "description": "simple javascript client for ReactPy's documentation",
  "main": "index.js",
  "scripts": {
    "build": "rollup --config",
    "format": "prettier --ignore-path .gitignore --write ."
  },
  "devDependencies": {
    "@rollup/plugin-commonjs": "^21.0.1",
    "@rollup/plugin-node-resolve": "^13.1.1",
    "@rollup/plugin-replace": "^3.0.0",
    "prettier": "^2.2.1",
    "rollup": "^2.35.1"
  },
  "dependencies": {
    "@reactpy/client": "file:../../../src/client/packages/@reactpy/client"
  }
}

docs/source/_custom_js/package-lock.json METASEP
{
  "name": "reactpy-docs-example-loader",
  "version": "1.0.0",
  "lockfileVersion": 2,
  "requires": true,
  "packages": {
    "": {
      "name": "reactpy-docs-example-loader",
      "version": "1.0.0",
      "dependencies": {
        "@reactpy/client": "file:../../../src/client/packages/@reactpy/client"
      },
      "devDependencies": {
        "@rollup/plugin-commonjs": "^21.0.1",
        "@rollup/plugin-node-resolve": "^13.1.1",
        "@rollup/plugin-replace": "^3.0.0",
        "prettier": "^2.2.1",
        "rollup": "^2.35.1"
      }
    },
    "../../../src/client/packages/@reactpy/client": {
      "version": "0.3.1",
      "integrity": "sha512-pIK5eNwFSHKXg7ClpASWFVKyZDYxz59MSFpVaX/OqJFkrJaAxBuhKGXNTMXmuyWOL5Iyvb/ErwwDRxQRzMNkfQ==",
      "license": "MIT",
      "dependencies": {
        "event-to-object": "^0.1.2",
        "json-pointer": "^0.6.2"
      },
      "devDependencies": {
        "@types/json-pointer": "^1.0.31",
        "@types/react": "^17.0",
        "@types/react-dom": "^17.0",
        "typescript": "^4.9.5"
      },
      "peerDependencies": {
        "react": ">=16 <18",
        "react-dom": ">=16 <18"
      }
    },
    "../../../src/client/packages/client": {
      "name": "@reactpy/client",
      "version": "0.2.0",
      "extraneous": true,
      "license": "MIT",
      "dependencies": {
        "event-to-object": "^0.1.0",
        "json-pointer": "^0.6.2"
      },
      "devDependencies": {
        "@types/json-pointer": "^1.0.31",
        "@types/react": "^17.0",
        "@types/react-dom": "^17.0",
        "prettier": "^3.0.0-alpha.6",
        "typescript": "^4.9.5"
      },
      "peerDependencies": {
        "react": ">=16 <18",
        "react-dom": ">=16 <18"
      }
    },
    "node_modules/@reactpy/client": {
      "resolved": "../../../src/client/packages/@reactpy/client",
      "link": true
    },
    "node_modules/@rollup/plugin-commonjs": {
      "version": "21.0.1",
      "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-21.0.1.tgz",
      "integrity": "sha512-EA+g22lbNJ8p5kuZJUYyhhDK7WgJckW5g4pNN7n4mAFUM96VuwUnNT3xr2Db2iCZPI1pJPbGyfT5mS9T1dHfMg==",
      "dev": true,
      "dependencies": {
        "@rollup/pluginutils": "^3.1.0",
        "commondir": "^1.0.1",
        "estree-walker": "^2.0.1",
        "glob": "^7.1.6",
        "is-reference": "^1.2.1",
        "magic-string": "^0.25.7",
        "resolve": "^1.17.0"
      },
      "engines": {
        "node": ">= 8.0.0"
      },
      "peerDependencies": {
        "rollup": "^2.38.3"
      }
    },
    "node_modules/@rollup/plugin-commonjs/node_modules/estree-walker": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
      "dev": true
    },
    "node_modules/@rollup/plugin-node-resolve": {
      "version": "13.1.1",
      "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.1.1.tgz",
      "integrity": "sha512-6QKtRevXLrmEig9UiMYt2fSvee9TyltGRfw+qSs6xjUnxwjOzTOqy+/Lpxsgjb8mJn1EQNbCDAvt89O4uzL5kw==",
      "dev": true,
      "dependencies": {
        "@rollup/pluginutils": "^3.1.0",
        "@types/resolve": "1.17.1",
        "builtin-modules": "^3.1.0",
        "deepmerge": "^4.2.2",
        "is-module": "^1.0.0",
        "resolve": "^1.19.0"
      },
      "engines": {
        "node": ">= 10.0.0"
      },
      "peerDependencies": {
        "rollup": "^2.42.0"
      }
    },
    "node_modules/@rollup/plugin-node-resolve/node_modules/@types/resolve": {
      "version": "1.17.1",
      "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
      "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==",
      "dev": true,
      "dependencies": {
        "@types/node": "*"
      }
    },
    "node_modules/@rollup/plugin-replace": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-3.0.0.tgz",
      "integrity": "sha512-3c7JCbMuYXM4PbPWT4+m/4Y6U60SgsnDT/cCyAyUKwFHg7pTSfsSQzIpETha3a3ig6OdOKzZz87D9ZXIK3qsDg==",
      "dev": true,
      "dependencies": {
        "@rollup/pluginutils": "^3.1.0",
        "magic-string": "^0.25.7"
      },
      "peerDependencies": {
        "rollup": "^1.20.0 || ^2.0.0"
      }
    },
    "node_modules/@rollup/pluginutils": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
      "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==",
      "dev": true,
      "dependencies": {
        "@types/estree": "0.0.39",
        "estree-walker": "^1.0.1",
        "picomatch": "^2.2.2"
      },
      "engines": {
        "node": ">= 8.0.0"
      },
      "peerDependencies": {
        "rollup": "^1.20.0||^2.0.0"
      }
    },
    "node_modules/@rollup/pluginutils/node_modules/@types/estree": {
      "version": "0.0.39",
      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
      "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
      "dev": true
    },
    "node_modules/@rollup/pluginutils/node_modules/estree-walker": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
      "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
      "dev": true
    },
    "node_modules/@types/estree": {
      "version": "0.0.48",
      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz",
      "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==",
      "dev": true
    },
    "node_modules/@types/node": {
      "version": "15.12.2",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz",
      "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==",
      "dev": true
    },
    "node_modules/balanced-match": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
      "dev": true
    },
    "node_modules/brace-expansion": {
      "version": "1.1.11",
      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
      "dev": true,
      "dependencies": {
        "balanced-match": "^1.0.0",
        "concat-map": "0.0.1"
      }
    },
    "node_modules/builtin-modules": {
      "version": "3.2.0",
      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
      "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==",
      "dev": true,
      "engines": {
        "node": ">=6"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/commondir": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
      "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
      "dev": true
    },
    "node_modules/concat-map": {
      "version": "0.0.1",
      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
      "dev": true
    },
    "node_modules/deepmerge": {
      "version": "4.2.2",
      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
      "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
      "dev": true,
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/fs.realpath": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
      "dev": true
    },
    "node_modules/fsevents": {
      "version": "2.3.2",
      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
      "dev": true,
      "hasInstallScript": true,
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
      }
    },
    "node_modules/function-bind": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
      "dev": true
    },
    "node_modules/glob": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
      "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
      "dev": true,
      "dependencies": {
        "fs.realpath": "^1.0.0",
        "inflight": "^1.0.4",
        "inherits": "2",
        "minimatch": "^3.0.4",
        "once": "^1.3.0",
        "path-is-absolute": "^1.0.0"
      },
      "engines": {
        "node": "*"
      },
      "funding": {
        "url": "https://github.com/sponsors/isaacs"
      }
    },
    "node_modules/has": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
      "dev": true,
      "dependencies": {
        "function-bind": "^1.1.1"
      },
      "engines": {
        "node": ">= 0.4.0"
      }
    },
    "node_modules/inflight": {
      "version": "1.0.6",
      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
      "dev": true,
      "dependencies": {
        "once": "^1.3.0",
        "wrappy": "1"
      }
    },
    "node_modules/inherits": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
      "dev": true
    },
    "node_modules/is-core-module": {
      "version": "2.4.0",
      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz",
      "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==",
      "dev": true,
      "dependencies": {
        "has": "^1.0.3"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/is-module": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
      "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
      "dev": true
    },
    "node_modules/is-reference": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
      "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
      "dev": true,
      "dependencies": {
        "@types/estree": "*"
      }
    },
    "node_modules/magic-string": {
      "version": "0.25.7",
      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
      "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
      "dev": true,
      "dependencies": {
        "sourcemap-codec": "^1.4.4"
      }
    },
    "node_modules/minimatch": {
      "version": "3.0.4",
      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
      "dev": true,
      "dependencies": {
        "brace-expansion": "^1.1.7"
      },
      "engines": {
        "node": "*"
      }
    },
    "node_modules/once": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
      "dev": true,
      "dependencies": {
        "wrappy": "1"
      }
    },
    "node_modules/path-is-absolute": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
      "dev": true,
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/path-parse": {
      "version": "1.0.7",
      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
      "dev": true
    },
    "node_modules/picomatch": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
      "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
      "dev": true,
      "engines": {
        "node": ">=8.6"
      },
      "funding": {
        "url": "https://github.com/sponsors/jonschlinkert"
      }
    },
    "node_modules/prettier": {
      "version": "2.3.1",
      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz",
      "integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==",
      "dev": true,
      "bin": {
        "prettier": "bin-prettier.js"
      },
      "engines": {
        "node": ">=10.13.0"
      }
    },
    "node_modules/resolve": {
      "version": "1.20.0",
      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
      "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
      "dev": true,
      "dependencies": {
        "is-core-module": "^2.2.0",
        "path-parse": "^1.0.6"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/rollup": {
      "version": "2.52.1",
      "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.1.tgz",
      "integrity": "sha512-/SPqz8UGnp4P1hq6wc9gdTqA2bXQXGx13TtoL03GBm6qGRI6Hm3p4Io7GeiHNLl0BsQAne1JNYY+q/apcY933w==",
      "dev": true,
      "bin": {
        "rollup": "dist/bin/rollup"
      },
      "engines": {
        "node": ">=10.0.0"
      },
      "optionalDependencies": {
        "fsevents": "~2.3.2"
      }
    },
    "node_modules/sourcemap-codec": {
      "version": "1.4.8",
      "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
      "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
      "dev": true
    },
    "node_modules/wrappy": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
      "dev": true
    }
  },
  "dependencies": {
    "@reactpy/client": {
      "version": "file:../../../src/client/packages/@reactpy/client",
      "requires": {
        "@types/json-pointer": "^1.0.31",
        "@types/react": "^17.0",
        "@types/react-dom": "^17.0",
        "event-to-object": "^0.1.2",
        "json-pointer": "^0.6.2",
        "typescript": "^4.9.5"
      }
    },
    "@rollup/plugin-commonjs": {
      "version": "21.0.1",
      "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-21.0.1.tgz",
      "integrity": "sha512-EA+g22lbNJ8p5kuZJUYyhhDK7WgJckW5g4pNN7n4mAFUM96VuwUnNT3xr2Db2iCZPI1pJPbGyfT5mS9T1dHfMg==",
      "dev": true,
      "requires": {
        "@rollup/pluginutils": "^3.1.0",
        "commondir": "^1.0.1",
        "estree-walker": "^2.0.1",
        "glob": "^7.1.6",
        "is-reference": "^1.2.1",
        "magic-string": "^0.25.7",
        "resolve": "^1.17.0"
      },
      "dependencies": {
        "estree-walker": {
          "version": "2.0.2",
          "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
          "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
          "dev": true
        }
      }
    },
    "@rollup/plugin-node-resolve": {
      "version": "13.1.1",
      "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.1.1.tgz",
      "integrity": "sha512-6QKtRevXLrmEig9UiMYt2fSvee9TyltGRfw+qSs6xjUnxwjOzTOqy+/Lpxsgjb8mJn1EQNbCDAvt89O4uzL5kw==",
      "dev": true,
      "requires": {
        "@rollup/pluginutils": "^3.1.0",
        "@types/resolve": "1.17.1",
        "builtin-modules": "^3.1.0",
        "deepmerge": "^4.2.2",
        "is-module": "^1.0.0",
        "resolve": "^1.19.0"
      },
      "dependencies": {
        "@types/resolve": {
          "version": "1.17.1",
          "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
          "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==",
          "dev": true,
          "requires": {
            "@types/node": "*"
          }
        }
      }
    },
    "@rollup/plugin-replace": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-3.0.0.tgz",
      "integrity": "sha512-3c7JCbMuYXM4PbPWT4+m/4Y6U60SgsnDT/cCyAyUKwFHg7pTSfsSQzIpETha3a3ig6OdOKzZz87D9ZXIK3qsDg==",
      "dev": true,
      "requires": {
        "@rollup/pluginutils": "^3.1.0",
        "magic-string": "^0.25.7"
      }
    },
    "@rollup/pluginutils": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
      "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==",
      "dev": true,
      "requires": {
        "@types/estree": "0.0.39",
        "estree-walker": "^1.0.1",
        "picomatch": "^2.2.2"
      },
      "dependencies": {
        "@types/estree": {
          "version": "0.0.39",
          "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
          "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
          "dev": true
        },
        "estree-walker": {
          "version": "1.0.1",
          "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
          "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
          "dev": true
        }
      }
    },
    "@types/estree": {
      "version": "0.0.48",
      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz",
      "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==",
      "dev": true
    },
    "@types/node": {
      "version": "15.12.2",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz",
      "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==",
      "dev": true
    },
    "balanced-match": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
      "dev": true
    },
    "brace-expansion": {
      "version": "1.1.11",
      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
      "dev": true,
      "requires": {
        "balanced-match": "^1.0.0",
        "concat-map": "0.0.1"
      }
    },
    "builtin-modules": {
      "version": "3.2.0",
      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
      "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==",
      "dev": true
    },
    "commondir": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
      "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
      "dev": true
    },
    "concat-map": {
      "version": "0.0.1",
      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
      "dev": true
    },
    "deepmerge": {
      "version": "4.2.2",
      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
      "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
      "dev": true
    },
    "fs.realpath": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
      "dev": true
    },
    "fsevents": {
      "version": "2.3.2",
      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
      "dev": true,
      "optional": true
    },
    "function-bind": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
      "dev": true
    },
    "glob": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
      "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
      "dev": true,
      "requires": {
        "fs.realpath": "^1.0.0",
        "inflight": "^1.0.4",
        "inherits": "2",
        "minimatch": "^3.0.4",
        "once": "^1.3.0",
        "path-is-absolute": "^1.0.0"
      }
    },
    "has": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
      "dev": true,
      "requires": {
        "function-bind": "^1.1.1"
      }
    },
    "inflight": {
      "version": "1.0.6",
      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
      "dev": true,
      "requires": {
        "once": "^1.3.0",
        "wrappy": "1"
      }
    },
    "inherits": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
      "dev": true
    },
    "is-core-module": {
      "version": "2.4.0",
      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz",
      "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==",
      "dev": true,
      "requires": {
        "has": "^1.0.3"
      }
    },
    "is-module": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
      "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
      "dev": true
    },
    "is-reference": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
      "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
      "dev": true,
      "requires": {
        "@types/estree": "*"
      }
    },
    "magic-string": {
      "version": "0.25.7",
      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
      "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
      "dev": true,
      "requires": {
        "sourcemap-codec": "^1.4.4"
      }
    },
    "minimatch": {
      "version": "3.0.4",
      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
      "dev": true,
      "requires": {
        "brace-expansion": "^1.1.7"
      }
    },
    "once": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
      "dev": true,
      "requires": {
        "wrappy": "1"
      }
    },
    "path-is-absolute": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
      "dev": true
    },
    "path-parse": {
      "version": "1.0.7",
      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
      "dev": true
    },
    "picomatch": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
      "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
      "dev": true
    },
    "prettier": {
      "version": "2.3.1",
      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz",
      "integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==",
      "dev": true
    },
    "resolve": {
      "version": "1.20.0",
      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
      "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
      "dev": true,
      "requires": {
        "is-core-module": "^2.2.0",
        "path-parse": "^1.0.6"
      }
    },
    "rollup": {
      "version": "2.52.1",
      "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.1.tgz",
      "integrity": "sha512-/SPqz8UGnp4P1hq6wc9gdTqA2bXQXGx13TtoL03GBm6qGRI6Hm3p4Io7GeiHNLl0BsQAne1JNYY+q/apcY933w==",
      "dev": true,
      "requires": {
        "fsevents": "~2.3.2"
      }
    },
    "sourcemap-codec": {
      "version": "1.4.8",
      "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
      "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
      "dev": true
    },
    "wrappy": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
      "dev": true
    }
  }
}

docs/source/_custom_js/README.md METASEP
# Custom Javascript for ReactPy's Docs

Build the javascript with

```
npm run build
```

This will drop a javascript bundle into `../_static/custom.js`

docs/source/_exts/reactpy_view.py METASEP
import os

from docutils.nodes import raw
from docutils.parsers.rst import directives
from sphinx.application import Sphinx
from sphinx.util.docutils import SphinxDirective

from docs.examples import get_normalized_example_name


_REACTPY_EXAMPLE_HOST = os.environ.get("REACTPY_DOC_EXAMPLE_SERVER_HOST", "")
_REACTPY_STATIC_HOST = os.environ.get("REACTPY_DOC_STATIC_SERVER_HOST", "/docs").rstrip(
    "/"
)


class IteractiveWidget(SphinxDirective):
    has_content = False
    required_arguments = 1
    _next_id = 0

    option_spec = {
        "activate-button": directives.flag,
        "margin": float,
    }

    def run(self):
        IteractiveWidget._next_id += 1
        container_id = f"reactpy-widget-{IteractiveWidget._next_id}"
        view_id = get_normalized_example_name(
            self.arguments[0],
            # only used if example name starts with "/"
            self.get_source_info()[0],
        )
        return [
            raw(
                "",
                f"""
                <div>
                    <div
                        id="{container_id}"
                        class="interactive widget-container"
                        style="margin-bottom: {self.options.get("margin", 0)}px;"
                    />
                    <script type="module">
                        import {{ mountWidgetExample }} from "{_REACTPY_STATIC_HOST}/_static/custom.js";
                        mountWidgetExample(
                            "{container_id}",
                            "{view_id}",
                            "{_REACTPY_EXAMPLE_HOST}",
                            {"true" if "activate-button" in self.options else "false"},
                        );
                    </script>
                </div>
                """,
                format="html",
            )
        ]


def setup(app: Sphinx) -> None:
    app.add_directive("reactpy-view", IteractiveWidget)

docs/source/_exts/reactpy_example.py METASEP
from __future__ import annotations

import re
from pathlib import Path
from typing import Any

from docutils.parsers.rst import directives
from docutils.statemachine import StringList
from sphinx.application import Sphinx
from sphinx.util.docutils import SphinxDirective
from sphinx_design.tabs import TabSetDirective

from docs.examples import (
    SOURCE_DIR,
    get_example_files_by_name,
    get_normalized_example_name,
)


class WidgetExample(SphinxDirective):
    has_content = False
    required_arguments = 1
    _next_id = 0

    option_spec = {
        "result-is-default-tab": directives.flag,
        "activate-button": directives.flag,
    }

    def run(self):
        example_name = get_normalized_example_name(
            self.arguments[0],
            # only used if example name starts with "/"
            self.get_source_info()[0],
        )

        show_linenos = "linenos" in self.options
        live_example_is_default_tab = "result-is-default-tab" in self.options
        activate_result = "activate-button" not in self.options

        ex_files = get_example_files_by_name(example_name)
        if not ex_files:
            src_file, line_num = self.get_source_info()
            raise ValueError(
                f"Missing example named {example_name!r} "
                f"referenced by document {src_file}:{line_num}"
            )

        labeled_tab_items: list[tuple[str, Any]] = []
        if len(ex_files) == 1:
            labeled_tab_items.append(
                (
                    "main.py",
                    _literal_include(
                        path=ex_files[0],
                        linenos=show_linenos,
                    ),
                )
            )
        else:
            for path in sorted(
                ex_files, key=lambda p: "" if p.name == "main.py" else p.name
            ):
                labeled_tab_items.append(
                    (
                        path.name,
                        _literal_include(
                            path=path,
                            linenos=show_linenos,
                        ),
                    )
                )

        result_tab_item = (
            "🚀 result",
            _interactive_widget(
                name=example_name,
                with_activate_button=not activate_result,
            ),
        )
        if live_example_is_default_tab:
            labeled_tab_items.insert(0, result_tab_item)
        else:
            labeled_tab_items.append(result_tab_item)

        return TabSetDirective(
            "WidgetExample",
            [],
            {},
            _make_tab_items(labeled_tab_items),
            self.lineno - 2,
            self.content_offset,
            "",
            self.state,
            self.state_machine,
        ).run()


def _make_tab_items(labeled_content_tuples):
    tab_items = ""
    for label, content in labeled_content_tuples:
        tab_items += _tab_item_template.format(
            label=label,
            content=content.replace("\n", "\n    "),
        )
    return _string_to_nested_lines(tab_items)


def _literal_include(path: Path, linenos: bool):
    try:
        language = {
            ".py": "python",
            ".js": "javascript",
            ".json": "json",
        }[path.suffix]
    except KeyError:
        raise ValueError(f"Unknown extension type {path.suffix!r}")

    return _literal_include_template.format(
        name=str(path.relative_to(SOURCE_DIR)),
        language=language,
        options=_join_options(_get_file_options(path)),
    )


def _join_options(option_strings: list[str]) -> str:
    return "\n    ".join(option_strings)


OPTION_PATTERN = re.compile(r"#\s:[\w-]+:.*")


def _get_file_options(file: Path) -> list[str]:
    options = []

    for line in file.read_text().split("\n"):
        if not line.strip():
            continue
        if not line.startswith("#"):
            break
        if not OPTION_PATTERN.match(line):
            continue
        option_string = line[1:].strip()
        if option_string:
            options.append(option_string)

    return options


def _interactive_widget(name, with_activate_button):
    return _interactive_widget_template.format(
        name=name,
        activate_button_opt=":activate-button:" if with_activate_button else "",
    )


_tab_item_template = """
.. tab-item:: {label}

    {content}
"""


_interactive_widget_template = """
.. reactpy-view:: {name}
    {activate_button_opt}
"""


_literal_include_template = """
.. literalinclude:: /{name}
    :language: {language}
    {options}
"""


def _string_to_nested_lines(content):
    return StringList(content.split("\n"))


def setup(app: Sphinx) -> None:
    app.add_directive("reactpy", WidgetExample)

docs/source/_exts/patched_html_translator.py METASEP
from sphinx.util.docutils import is_html5_writer_available
from sphinx.writers.html import HTMLTranslator
from sphinx.writers.html5 import HTML5Translator


class PatchedHTMLTranslator(
    HTML5Translator if is_html5_writer_available() else HTMLTranslator
):
    def starttag(self, node, tagname, *args, **attrs):
        if (
            tagname == "a"
            and "target" not in attrs
            and (
                "external" in attrs.get("class", "")
                or "external" in attrs.get("classes", [])
            )
        ):
            attrs["target"] = "_blank"
            attrs["ref"] = "noopener noreferrer"
        return super().starttag(node, tagname, *args, **attrs)


def setup(app):
    app.set_translator("html", PatchedHTMLTranslator)

docs/source/_exts/custom_autosectionlabel.py METASEP
"""Mostly copied from sphinx.ext.autosectionlabel

See Sphinx BSD license:
https://github.com/sphinx-doc/sphinx/blob/f9968594206e538f13fa1c27c065027f10d4ea27/LICENSE
"""

from __future__ import annotations

from fnmatch import fnmatch
from typing import Any, cast

from docutils import nodes
from docutils.nodes import Node
from sphinx.application import Sphinx
from sphinx.domains.std import StandardDomain
from sphinx.locale import __
from sphinx.util import logging
from sphinx.util.nodes import clean_astext


logger = logging.getLogger(__name__)


def get_node_depth(node: Node) -> int:
    i = 0
    cur_node = node
    while cur_node.parent != node.document:
        cur_node = cur_node.parent
        i += 1
    return i


def register_sections_as_label(app: Sphinx, document: Node) -> None:
    docname = app.env.docname

    for pattern in app.config.autosectionlabel_skip_docs:
        if fnmatch(docname, pattern):
            return None

    domain = cast(StandardDomain, app.env.get_domain("std"))
    for node in document.traverse(nodes.section):
        if (
            app.config.autosectionlabel_maxdepth
            and get_node_depth(node) >= app.config.autosectionlabel_maxdepth
        ):
            continue
        labelid = node["ids"][0]

        title = cast(nodes.title, node[0])
        ref_name = getattr(title, "rawsource", title.astext())
        if app.config.autosectionlabel_prefix_document:
            name = nodes.fully_normalize_name(docname + ":" + ref_name)
        else:
            name = nodes.fully_normalize_name(ref_name)
        sectname = clean_astext(title)

        if name in domain.labels:
            logger.warning(
                __("duplicate label %s, other instance in %s"),
                name,
                app.env.doc2path(domain.labels[name][0]),
                location=node,
                type="autosectionlabel",
                subtype=docname,
            )

        domain.anonlabels[name] = docname, labelid
        domain.labels[name] = docname, labelid, sectname


def setup(app: Sphinx) -> dict[str, Any]:
    app.add_config_value("autosectionlabel_prefix_document", False, "env")
    app.add_config_value("autosectionlabel_maxdepth", None, "env")
    app.add_config_value("autosectionlabel_skip_docs", [], "env")
    app.connect("doctree-read", register_sections_as_label)

    return {
        "version": "builtin",
        "parallel_read_safe": True,
        "parallel_write_safe": True,
    }

docs/source/_exts/copy_vdom_json_schema.py METASEP
import json
from pathlib import Path

from sphinx.application import Sphinx

from reactpy.core.vdom import VDOM_JSON_SCHEMA


def setup(app: Sphinx) -> None:
    schema_file = Path(__file__).parent.parent / "vdom-json-schema.json"
    current_schema = json.dumps(VDOM_JSON_SCHEMA, indent=2, sort_keys=True)

    # We need to make this check because the autoreload system for the docs checks
    # to see if the file has changed to determine whether to re-build. Thus we should
    # only write to the file if its contents will be different.
    if not schema_file.exists() or schema_file.read_text() != current_schema:
        schema_file.write_text(current_schema)

docs/source/_exts/build_custom_js.py METASEP
import subprocess
from pathlib import Path

from sphinx.application import Sphinx


SOURCE_DIR = Path(__file__).parent.parent
CUSTOM_JS_DIR = SOURCE_DIR / "_custom_js"


def setup(app: Sphinx) -> None:
    subprocess.run("npm install", cwd=CUSTOM_JS_DIR, shell=True)
    subprocess.run("npm run build", cwd=CUSTOM_JS_DIR, shell=True)

docs/source/_exts/autogen_api_docs.py METASEP
from __future__ import annotations

import sys
from pathlib import Path
from typing import Collection, Iterator

from sphinx.application import Sphinx


HERE = Path(__file__).parent
SRC = HERE.parent.parent.parent / "src"
PYTHON_PACKAGE = SRC / "reactpy"

AUTO_DIR = HERE.parent / "_auto"
AUTO_DIR.mkdir(exist_ok=True)

API_FILE = AUTO_DIR / "apis.rst"

# All valid RST section symbols - it shouldn't be realistically possible to exhaust them
SECTION_SYMBOLS = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""

AUTODOC_TEMPLATE_WITH_MEMBERS = """\
.. automodule:: {module}
    :members:
    :ignore-module-all:
"""

AUTODOC_TEMPLATE_WITHOUT_MEMBERS = """\
.. automodule:: {module}
    :ignore-module-all:
"""

TITLE = """\
==========
Python API
==========
"""


def generate_api_docs():
    content = [TITLE]

    for file in walk_python_files(PYTHON_PACKAGE, ignore_dirs={"__pycache__"}):
        if file.name == "__init__.py":
            if file.parent != PYTHON_PACKAGE:
                content.append(make_package_section(file))
        else:
            content.append(make_module_section(file))

    API_FILE.write_text("\n".join(content))


def make_package_section(file: Path) -> str:
    parent_dir = file.parent
    symbol = get_section_symbol(parent_dir)
    section_name = f"``{parent_dir.name}``"
    module_name = get_module_name(parent_dir)
    return (
        section_name
        + "\n"
        + (symbol * len(section_name))
        + "\n"
        + AUTODOC_TEMPLATE_WITHOUT_MEMBERS.format(module=module_name)
    )


def make_module_section(file: Path) -> str:
    symbol = get_section_symbol(file)
    section_name = f"``{file.stem}``"
    module_name = get_module_name(file)
    return (
        section_name
        + "\n"
        + (symbol * len(section_name))
        + "\n"
        + AUTODOC_TEMPLATE_WITH_MEMBERS.format(module=module_name)
    )


def get_module_name(path: Path) -> str:
    return ".".join(path.with_suffix("").relative_to(PYTHON_PACKAGE.parent).parts)


def get_section_symbol(path: Path) -> str:
    rel_path_parts = path.relative_to(PYTHON_PACKAGE).parts
    assert len(rel_path_parts) < len(SECTION_SYMBOLS), "package structure is too deep"
    return SECTION_SYMBOLS[len(rel_path_parts)]


def walk_python_files(root: Path, ignore_dirs: Collection[str]) -> Iterator[Path]:
    """Iterate over Python files

    We yield in a particular order to get the correction title section structure. Given
    a directory structure of the form::

        project/
            __init__.py
            /package
                __init__.py
                module_a.py
            module_b.py

    We yield the files in this order::

        project/__init__.py
        project/package/__init__.py
        project/package/module_a.py
        project/module_b.py

    In this way we generate the section titles in the appropriate order::

        project
        =======

        project.package
        ---------------

        project.package.module_a
        ------------------------

    """
    for path in sorted(
        root.iterdir(),
        key=lambda path: (
            # __init__.py files first
            int(not path.name == "__init__.py"),
            # then directories
            int(not path.is_dir()),
            # sort by file name last
            path.name,
        ),
    ):
        if path.is_dir():
            if (path / "__init__.py").exists() and path.name not in ignore_dirs:
                yield from walk_python_files(path, ignore_dirs)
        elif path.suffix == ".py":
            yield path


def setup(app: Sphinx) -> None:
    if sys.platform == "win32" and sys.version_info[:2] == (3, 7):
        return None
    generate_api_docs()
    return None

docs/source/_exts/async_doctest.py METASEP
from doctest import DocTest, DocTestRunner
from textwrap import indent
from typing import Any

from sphinx.application import Sphinx
from sphinx.ext.doctest import DocTestBuilder
from sphinx.ext.doctest import setup as doctest_setup


test_template = """
import asyncio as __test_template_asyncio

async def __test_template__main():

    {test}

    globals().update(locals())

__test_template_asyncio.run(__test_template__main())
"""


class TestRunnerWrapper:
    def __init__(self, runner: DocTestRunner):
        self._runner = runner

    def __getattr__(self, name: str) -> Any:
        return getattr(self._runner, name)

    def run(self, test: DocTest, *args: Any, **kwargs: Any) -> Any:
        for ex in test.examples:
            ex.source = test_template.format(test=indent(ex.source, "    ").strip())
        return self._runner.run(test, *args, **kwargs)


class AsyncDoctestBuilder(DocTestBuilder):
    @property
    def test_runner(self) -> DocTestRunner:
        return self._test_runner

    @test_runner.setter
    def test_runner(self, value: DocTestRunner) -> None:
        self._test_runner = TestRunnerWrapper(value)
        return None


def setup(app: Sphinx) -> None:
    doctest_setup(app)
    app.add_builder(AsyncDoctestBuilder, override=True)
    return None

docs/source/index.rst METASEP
.. card::

    This documentation is still under construction 🚧. We welcome your `feedback
    <https://github.com/reactive-python/reactpy/discussions>`__!


ReactPy
=======

.. toctree::
    :hidden:
    :caption: Guides

    guides/getting-started/index
    guides/creating-interfaces/index
    guides/adding-interactivity/index
    guides/managing-state/index
    guides/escape-hatches/index
    guides/understanding-reactpy/index

.. toctree::
    :hidden:
    :caption: Reference

    reference/browser-events
    reference/html-attributes
    reference/hooks-api
    _auto/apis
    reference/javascript-api
    reference/specifications

.. toctree::
    :hidden:
    :caption: About

    about/changelog
    about/contributor-guide
    about/credits-and-licenses
    Source Code <https://github.com/reactive-python/reactpy>
    Community <https://github.com/reactive-python/reactpy/discussions>

ReactPy is a library for building user interfaces in Python without Javascript. ReactPy
interfaces are made from :ref:`components <Your First Components>` which look and behave
similarly to those found in `ReactJS <https://reactjs.org/>`__. Designed with simplicity
in mind, ReactPy can be used by those without web development experience while also
being powerful enough to grow with your ambitions.


At a Glance
-----------

To get a rough idea of how to write apps in ReactPy, take a look at the tiny `"hello world"
<https://en.wikipedia.org/wiki/%22Hello,_World!%22_program>`__ application below:

.. reactpy:: guides/getting-started/_examples/hello_world

.. hint::

    Try clicking the **🚀 result** tab to see what this displays!

So what exactly does this code do? First, it imports a few tools from ``reactpy`` that will
get used to describe and execute an application. Then, we create an ``App`` function
which will define the content the application displays. Specifically, it displays a kind
of HTML element called an ``h1`` `section heading
<https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Heading_Elements>`__.
Importantly though, a ``@component`` decorator has been applied to the ``App`` function
to turn it into a :ref:`component <Your First Components>`. Finally, we :ref:`run
<Running ReactPy>` a development web server by passing the ``App`` component to the
``run()`` function.

.. note::

    See :ref:`Running ReactPy in Production` to learn how to use a production-grade server
    to run ReactPy.


Learning ReactPy
----------------

This documentation is broken up into chapters and sections that introduce you to
concepts step by step with detailed explanations and lots of examples. You should feel
free to dive into any content that seems interesting. While each chapter assumes
knowledge from those that came before, when you encounter a concept you're unfamiliar
with you should look for links that will help direct you to the place where it was
originally taught.


Chapter 1 - :ref:`Getting Started`
-----------------------------------

If you want to follow along with examples in the sections that follow, you'll want to
start here so you can :ref:`install ReactPy <Installing ReactPy>`. This section also contains
more detailed information about how to :ref:`run ReactPy <Running ReactPy>` in different
contexts. For example, if you want to embed ReactPy into an existing application, or run
ReactPy within a Jupyter Notebook, this is where you can learn how to do those things.

.. grid:: 1 2 2 2

    .. grid-item::

        .. image:: _static/install-and-run-reactpy.gif

    .. grid-item::

        .. image:: guides/getting-started/_static/reactpy-in-jupyterlab.gif

.. card::
    :link: guides/getting-started/index
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    Install ReactPy and run it in a variety of different ways - with different web servers
    and frameworks. You'll even embed ReactPy into an existing app.


Chapter 2 - :ref:`Creating Interfaces`
--------------------------------------

ReactPy is a Python package for making user interfaces (UI). These interfaces are built
from small elements of functionality like buttons text and images. ReactPy allows you to
combine these elements into reusable :ref:`"components" <your first components>`. In the
sections that follow you'll learn how these UI elements are created and organized into
components. Then, you'll use this knowledge to create interfaces from raw data:

.. reactpy:: guides/creating-interfaces/rendering-data/_examples/todo_list_with_keys

.. card::
    :link: guides/creating-interfaces/index
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    Learn to construct user interfaces from basic HTML elements and reusable components.


Chapter 3 - :ref:`Adding Interactivity`
---------------------------------------

Components often need to change what’s on the screen as a result of an interaction. For
example, typing into the form should update the input field, clicking a “Comment” button
should bring up a text input field, clicking “Buy” should put a product in the shopping
cart. Components need to “remember” things like the current input value, the current
image, the shopping cart. In ReactPy, this kind of component-specific memory is created and
updated with a "hook" called ``use_state()`` that creates a **state variable** and
**state setter** respectively:

.. reactpy:: guides/adding-interactivity/components-with-state/_examples/adding_state_variable

In ReactPy, ``use_state``, as well as any other function whose name starts with ``use``, is
called a "hook". These are special functions that should only be called while ReactPy is
:ref:`rendering <the rendering process>`. They let you "hook into" the different
capabilities of ReactPy's components of which ``use_state`` is just one (well get into the
other :ref:`later <managing state>`).

.. card::
    :link: guides/adding-interactivity/index
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    Learn how user interfaces can be made to respond to user interaction in real-time.


Chapter 4 - :ref:`Managing State`
---------------------------------

.. card::
    :link: guides/managing-state/index
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    Under construction 🚧



Chapter 5 - :ref:`Escape Hatches`
---------------------------------

.. card::
    :link: guides/escape-hatches/index
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    Under construction 🚧


Chapter 6 - :ref:`Understanding ReactPy`
----------------------------------------

.. card::
    :link: guides/escape-hatches/index
    :link-type: doc

    :octicon:`book` Read More
    ^^^^^^^^^^^^^^^^^^^^^^^^^

    Under construction 🚧


branding/svg/reactpy-logo-square.svg METASEP
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
   width="1000"
   height="1000"
   viewBox="0 0 264.58 264.58"
   version="1.1"
   id="svg890"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:svg="http://www.w3.org/2000/svg">
  <defs
     id="defs2" />
  <g
     id="layer1"
     transform="matrix(1.1968103,0,0,1.221235,-22.051826,-29.206699)"
     style="fill:#00be5a;fill-opacity:1">
    <g
       id="g37902"
       style="fill:#00be5a;fill-opacity:1">
      <circle
         style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:6.61449;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
         id="path14082"
         cx="132.28712"
         cy="132.28716"
         r="16.536238" />
      <g
         id="g31972"
         style="fill:#00be5a;fill-opacity:1">
        <path
           style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.55391;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
           id="path15079-9-6-2"
           transform="rotate(150)"
           d="m -48.790388,-278.76829 c 1.375733,0.38008 3.778011,1.69906 6.747005,3.78375 0.413023,0.29 0.847811,0.60231 1.300374,0.93557 1.26955,0.93489 2.876917,2.21115 4.689357,4.04813 1.745816,1.76945 3.714063,4.08962 5.797099,7.198 1.753533,2.61668 3.582194,5.78446 5.400025,9.60883 3.149699,6.62638 6.227597,15.15117 8.70143,25.7326 2.21665,9.48138 3.907031,20.45533 4.684311,32.68964 1.4914398,23.47515 -0.715185,46.65785 -5.805647,65.7865 -2.588599,9.7273 -5.814892,17.99733 -9.417093,24.59797 -1.890488,3.46411 -3.853175,6.411905 -5.843267,8.866476 -2.104906,2.59618 -4.204339,4.596491 -6.245487,6.08719 -2.051741,1.498435 -4.060366,2.504859 -6.005794,3.069223 -1.840769,0.534001 -3.662431,0.683925 -5.481457,0.45655 -1.764571,-0.220567 -3.612136,-0.807338 -5.533666,-1.846961 -1.887895,-1.021425 -3.846757,-2.480677 -5.834665,-4.444432 -3.952422,-3.904402 -7.83097,-9.618406 -11.31471,-17.156686 -3.524148,-7.62572 -6.50198,-16.80959 -8.748861,-27.26278 -5.091735,-23.68835 -5.202376,-47.76946 -3.204601,-66.67525 1.364944,-12.91704 3.647167,-23.1336 5.78832,-30.55392 0.583592,-2.02248 1.156821,-3.8388 1.698441,-5.4505 3.450532,-10.26773 6.988836,-16.35101 8.387316,-18.66612 l -8.466458,-5.11429 c -1.655846,2.74117 -5.553619,9.4909 -9.296835,20.62956 -0.584111,1.73813 -1.201238,3.68757 -1.828899,5.84963 -2.304715,7.93888 -4.738337,18.71499 -6.211046,32.24007 -2.147162,19.71914 -2.129523,44.90359 3.184464,69.86869 2.349584,11.03835 5.524422,20.97173 9.420182,29.41818 3.831707,8.307568 8.350964,15.153101 13.430329,20.144979 2.576889,2.532504 5.312866,4.602366 8.188816,6.136377 2.947667,1.572265 6.016664,2.567182 9.155763,2.925666 3.252611,0.371448 6.458413,0.04557 9.547901,-0.90012 3.229096,-0.988431 6.247863,-2.627023 9.037488,-4.737875 2.777282,-2.101511 5.433965,-4.740086 7.95145,-7.905101 2.380537,-2.992843 4.634422,-6.453877 6.740464,-10.363173 4.002689,-7.429913 7.455191,-16.452953 10.1671393,-26.762323 5.3252774,-20.24384 7.54173605,-44.48566 5.8242559,-68.86016 -0.8971905,-12.73294 -2.7776961,-24.20959 -5.2759074,-34.17451 -2.7874038,-11.11846 -6.2980878,-20.1928 -10.0406478,-27.31206 -2.161419,-4.11154 -4.38578,-7.54655 -6.591639,-10.38984 -2.624708,-3.38317 -5.195626,-5.89373 -7.582928,-7.73618 -2.49073,-1.92226 -4.749815,-3.09263 -6.610334,-3.77729 -0.658039,-0.24215 -1.292684,-0.42971 -1.89931,-0.57217 -4.36639,-1.02538 -7.442495,0.25932 -8.602678,0.58816 z" />
        <g
           id="g27478"
           transform="translate(-4.8406107,-166.8366)"
           style="fill:#00be5a;fill-opacity:1">
          <g
             id="g2188-2-9-4-3"
             style="fill:#00be5a;fill-opacity:1"
             transform="matrix(6.9641189e-8,14.644994,-19.336741,9.1951804e-8,300.32226,141.46704)">
            <path
               style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
               d="m 16.13938,5.3266 h 0.26458 c 0.12354,0 0.247406,0.06176 0.344383,0.1264118 0.05591,0.046865 0.141622,0.084894 0.173603,0.1006421 0.0127,0.003 0.01117,0.00253 0.01117,0.00253 0,0 8.03e-4,0.00145 -0.003,-0.011157 -0.01774,-0.0315 -0.07352,-0.1135646 -0.142944,-0.1502587 C 16.685714,5.3271298 16.545,5.2566 16.40396,5.2566 h -0.26458 z"
               id="path1101-6-9-2-83" />
            <path
               style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
               d="m 16.13938,5.2816 h 0.26458 c 0.14104,0 0.281754,-0.07053 0.383212,-0.1381682 0.06942,-0.036694 0.125201,-0.118759 0.142971,-0.1502545 0.0038,-0.012612 0.003,-0.011157 0.003,-0.011157 0,0 0.0015,-4.753e-4 -0.01117,0.00253 -0.03198,0.015748 -0.117689,0.053778 -0.173603,0.1006421 C 16.651366,5.1498398 16.5275,5.2116 16.40396,5.2116 h -0.26458 z"
               id="path1101-4-7-6-2-33" />
          </g>
          <path
             id="rect3698-3-3-9-6-5-0-2-8"
             style="vector-effect:none;fill:#00be5a;fill-opacity:1;fill-rule:evenodd;stroke-width:3.91082;stroke-linejoin:bevel;stop-color:#000000"
             d="m 207.28366,355.53032 -3.38322,20.9263 c -0.309,1.91125 -1.95584,3.48772 -4.38527,3.48772 h -2.00402 c -2.42941,0 -4.07625,-1.57647 -4.38525,-3.48772 l -3.38322,-20.9263 c -0.309,-1.91125 1.95582,-3.48772 4.38525,-3.48772 h 8.77048 c 2.42943,0 4.69425,1.57646 4.38525,3.48772 z" />
          <circle
             style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path22550-4-0-0"
             cx="204.2769"
             cy="366.4433"
             r="2.0954933" />
          <circle
             style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path22550-4-2-5-4"
             cx="192.59309"
             cy="366.4433"
             r="2.0954933" />
        </g>
      </g>
      <g
         id="g31972-9"
         transform="rotate(120,132.28991,132.29016)"
         style="fill:#00be5a;fill-opacity:1">
        <path
           style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.55391;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
           id="path15079-9-6-2-0"
           transform="rotate(150)"
           d="m -48.790388,-278.76829 c 1.375733,0.38008 3.778011,1.69906 6.747005,3.78375 0.413023,0.29 0.847811,0.60231 1.300374,0.93557 1.26955,0.93489 2.876917,2.21115 4.689357,4.04813 1.745816,1.76945 3.714063,4.08962 5.797099,7.198 1.753533,2.61668 3.582194,5.78446 5.400025,9.60883 3.149699,6.62638 6.227597,15.15117 8.70143,25.7326 2.21665,9.48138 3.907031,20.45533 4.684311,32.68964 1.4914398,23.47515 -0.715185,46.65785 -5.805647,65.7865 -2.588599,9.7273 -5.814892,17.99733 -9.417093,24.59797 -1.890488,3.46411 -3.853175,6.411905 -5.843267,8.866476 -2.104906,2.59618 -4.204339,4.596491 -6.245487,6.08719 -2.051741,1.498435 -4.060366,2.504859 -6.005794,3.069223 -1.840769,0.534001 -3.662431,0.683925 -5.481457,0.45655 -1.764571,-0.220567 -3.612136,-0.807338 -5.533666,-1.846961 -1.887895,-1.021425 -3.846757,-2.480677 -5.834665,-4.444432 -3.952422,-3.904402 -7.83097,-9.618406 -11.31471,-17.156686 -3.524148,-7.62572 -6.50198,-16.80959 -8.748861,-27.26278 -5.091735,-23.68835 -5.202376,-47.76946 -3.204601,-66.67525 1.364944,-12.91704 3.647167,-23.1336 5.78832,-30.55392 0.583592,-2.02248 1.156821,-3.8388 1.698441,-5.4505 3.450532,-10.26773 6.988836,-16.35101 8.387316,-18.66612 l -8.466458,-5.11429 c -1.655846,2.74117 -5.553619,9.4909 -9.296835,20.62956 -0.584111,1.73813 -1.201238,3.68757 -1.828899,5.84963 -2.304715,7.93888 -4.738337,18.71499 -6.211046,32.24007 -2.147162,19.71914 -2.129523,44.90359 3.184464,69.86869 2.349584,11.03835 5.524422,20.97173 9.420182,29.41818 3.831707,8.307568 8.350964,15.153101 13.430329,20.144979 2.576889,2.532504 5.312866,4.602366 8.188816,6.136377 2.947667,1.572265 6.016664,2.567182 9.155763,2.925666 3.252611,0.371448 6.458413,0.04557 9.547901,-0.90012 3.229096,-0.988431 6.247863,-2.627023 9.037488,-4.737875 2.777282,-2.101511 5.433965,-4.740086 7.95145,-7.905101 2.380537,-2.992843 4.634422,-6.453877 6.740464,-10.363173 4.002689,-7.429913 7.455191,-16.452953 10.1671393,-26.762323 5.3252774,-20.24384 7.54173605,-44.48566 5.8242559,-68.86016 -0.8971905,-12.73294 -2.7776961,-24.20959 -5.2759074,-34.17451 -2.7874038,-11.11846 -6.2980878,-20.1928 -10.0406478,-27.31206 -2.161419,-4.11154 -4.38578,-7.54655 -6.591639,-10.38984 -2.624708,-3.38317 -5.195626,-5.89373 -7.582928,-7.73618 -2.49073,-1.92226 -4.749815,-3.09263 -6.610334,-3.77729 -0.658039,-0.24215 -1.292684,-0.42971 -1.89931,-0.57217 -4.36639,-1.02538 -7.442495,0.25932 -8.602678,0.58816 z" />
        <g
           id="g27478-6"
           transform="translate(-4.8406107,-166.8366)"
           style="fill:#00be5a;fill-opacity:1">
          <g
             id="g2188-2-9-4-3-8"
             style="fill:#00be5a;fill-opacity:1"
             transform="matrix(6.9641189e-8,14.644994,-19.336741,9.1951804e-8,300.32226,141.46704)">
            <path
               style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
               d="m 16.13938,5.3266 h 0.26458 c 0.12354,0 0.247406,0.06176 0.344383,0.1264118 0.05591,0.046865 0.141622,0.084894 0.173603,0.1006421 0.0127,0.003 0.01117,0.00253 0.01117,0.00253 0,0 8.03e-4,0.00145 -0.003,-0.011157 -0.01774,-0.0315 -0.07352,-0.1135646 -0.142944,-0.1502587 C 16.685714,5.3271298 16.545,5.2566 16.40396,5.2566 h -0.26458 z"
               id="path1101-6-9-2-83-7" />
            <path
               style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
               d="m 16.13938,5.2816 h 0.26458 c 0.14104,0 0.281754,-0.07053 0.383212,-0.1381682 0.06942,-0.036694 0.125201,-0.118759 0.142971,-0.1502545 0.0038,-0.012612 0.003,-0.011157 0.003,-0.011157 0,0 0.0015,-4.753e-4 -0.01117,0.00253 -0.03198,0.015748 -0.117689,0.053778 -0.173603,0.1006421 C 16.651366,5.1498398 16.5275,5.2116 16.40396,5.2116 h -0.26458 z"
               id="path1101-4-7-6-2-33-9" />
          </g>
          <path
             id="rect3698-3-3-9-6-5-0-2-8-0"
             style="vector-effect:none;fill:#00be5a;fill-opacity:1;fill-rule:evenodd;stroke-width:3.91082;stroke-linejoin:bevel;stop-color:#000000"
             d="m 207.28366,355.53032 -3.38322,20.9263 c -0.309,1.91125 -1.95584,3.48772 -4.38527,3.48772 h -2.00402 c -2.42941,0 -4.07625,-1.57647 -4.38525,-3.48772 l -3.38322,-20.9263 c -0.309,-1.91125 1.95582,-3.48772 4.38525,-3.48772 h 8.77048 c 2.42943,0 4.69425,1.57646 4.38525,3.48772 z" />
          <circle
             style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path22550-4-0-0-3"
             cx="204.2769"
             cy="366.4433"
             r="2.0954933" />
          <circle
             style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path22550-4-2-5-4-3"
             cx="192.59309"
             cy="366.4433"
             r="2.0954933" />
        </g>
      </g>
      <g
         id="g31972-2"
         transform="rotate(-120,132.29009,132.29016)"
         style="fill:#00be5a;fill-opacity:1">
        <path
           style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.55391;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
           id="path15079-9-6-2-6"
           transform="rotate(150)"
           d="m -48.790388,-278.76829 c 1.375733,0.38008 3.778011,1.69906 6.747005,3.78375 0.413023,0.29 0.847811,0.60231 1.300374,0.93557 1.26955,0.93489 2.876917,2.21115 4.689357,4.04813 1.745816,1.76945 3.714063,4.08962 5.797099,7.198 1.753533,2.61668 3.582194,5.78446 5.400025,9.60883 3.149699,6.62638 6.227597,15.15117 8.70143,25.7326 2.21665,9.48138 3.907031,20.45533 4.684311,32.68964 1.4914398,23.47515 -0.715185,46.65785 -5.805647,65.7865 -2.588599,9.7273 -5.814892,17.99733 -9.417093,24.59797 -1.890488,3.46411 -3.853175,6.411905 -5.843267,8.866476 -2.104906,2.59618 -4.204339,4.596491 -6.245487,6.08719 -2.051741,1.498435 -4.060366,2.504859 -6.005794,3.069223 -1.840769,0.534001 -3.662431,0.683925 -5.481457,0.45655 -1.764571,-0.220567 -3.612136,-0.807338 -5.533666,-1.846961 -1.887895,-1.021425 -3.846757,-2.480677 -5.834665,-4.444432 -3.952422,-3.904402 -7.83097,-9.618406 -11.31471,-17.156686 -3.524148,-7.62572 -6.50198,-16.80959 -8.748861,-27.26278 -5.091735,-23.68835 -5.202376,-47.76946 -3.204601,-66.67525 1.364944,-12.91704 3.647167,-23.1336 5.78832,-30.55392 0.583592,-2.02248 1.156821,-3.8388 1.698441,-5.4505 3.450532,-10.26773 6.988836,-16.35101 8.387316,-18.66612 l -8.466458,-5.11429 c -1.655846,2.74117 -5.553619,9.4909 -9.296835,20.62956 -0.584111,1.73813 -1.201238,3.68757 -1.828899,5.84963 -2.304715,7.93888 -4.738337,18.71499 -6.211046,32.24007 -2.147162,19.71914 -2.129523,44.90359 3.184464,69.86869 2.349584,11.03835 5.524422,20.97173 9.420182,29.41818 3.831707,8.307568 8.350964,15.153101 13.430329,20.144979 2.576889,2.532504 5.312866,4.602366 8.188816,6.136377 2.947667,1.572265 6.016664,2.567182 9.155763,2.925666 3.252611,0.371448 6.458413,0.04557 9.547901,-0.90012 3.229096,-0.988431 6.247863,-2.627023 9.037488,-4.737875 2.777282,-2.101511 5.433965,-4.740086 7.95145,-7.905101 2.380537,-2.992843 4.634422,-6.453877 6.740464,-10.363173 4.002689,-7.429913 7.455191,-16.452953 10.1671393,-26.762323 5.3252774,-20.24384 7.54173605,-44.48566 5.8242559,-68.86016 -0.8971905,-12.73294 -2.7776961,-24.20959 -5.2759074,-34.17451 -2.7874038,-11.11846 -6.2980878,-20.1928 -10.0406478,-27.31206 -2.161419,-4.11154 -4.38578,-7.54655 -6.591639,-10.38984 -2.624708,-3.38317 -5.195626,-5.89373 -7.582928,-7.73618 -2.49073,-1.92226 -4.749815,-3.09263 -6.610334,-3.77729 -0.658039,-0.24215 -1.292684,-0.42971 -1.89931,-0.57217 -4.36639,-1.02538 -7.442495,0.25932 -8.602678,0.58816 z" />
        <g
           id="g27478-5"
           transform="translate(-4.8406107,-166.8366)"
           style="fill:#00be5a;fill-opacity:1">
          <g
             id="g2188-2-9-4-3-2"
             style="fill:#00be5a;fill-opacity:1"
             transform="matrix(6.9641189e-8,14.644994,-19.336741,9.1951804e-8,300.32226,141.46704)">
            <path
               style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
               d="m 16.13938,5.3266 h 0.26458 c 0.12354,0 0.247406,0.06176 0.344383,0.1264118 0.05591,0.046865 0.141622,0.084894 0.173603,0.1006421 0.0127,0.003 0.01117,0.00253 0.01117,0.00253 0,0 8.03e-4,0.00145 -0.003,-0.011157 -0.01774,-0.0315 -0.07352,-0.1135646 -0.142944,-0.1502587 C 16.685714,5.3271298 16.545,5.2566 16.40396,5.2566 h -0.26458 z"
               id="path1101-6-9-2-83-6" />
            <path
               style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
               d="m 16.13938,5.2816 h 0.26458 c 0.14104,0 0.281754,-0.07053 0.383212,-0.1381682 0.06942,-0.036694 0.125201,-0.118759 0.142971,-0.1502545 0.0038,-0.012612 0.003,-0.011157 0.003,-0.011157 0,0 0.0015,-4.753e-4 -0.01117,0.00253 -0.03198,0.015748 -0.117689,0.053778 -0.173603,0.1006421 C 16.651366,5.1498398 16.5275,5.2116 16.40396,5.2116 h -0.26458 z"
               id="path1101-4-7-6-2-33-5" />
          </g>
          <path
             id="rect3698-3-3-9-6-5-0-2-8-8"
             style="vector-effect:none;fill:#00be5a;fill-opacity:1;fill-rule:evenodd;stroke-width:3.91082;stroke-linejoin:bevel;stop-color:#000000"
             d="m 207.28366,355.53032 -3.38322,20.9263 c -0.309,1.91125 -1.95584,3.48772 -4.38527,3.48772 h -2.00402 c -2.42941,0 -4.07625,-1.57647 -4.38525,-3.48772 l -3.38322,-20.9263 c -0.309,-1.91125 1.95582,-3.48772 4.38525,-3.48772 h 8.77048 c 2.42943,0 4.69425,1.57646 4.38525,3.48772 z" />
          <circle
             style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path22550-4-0-0-7"
             cx="204.2769"
             cy="366.4433"
             r="2.0954933" />
          <circle
             style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path22550-4-2-5-4-9"
             cx="192.59309"
             cy="366.4433"
             r="2.0954933" />
        </g>
      </g>
    </g>
  </g>
</svg>

branding/svg/reactpy-logo-square-padded.svg METASEP
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
   width="1000"
   height="1000"
   viewBox="0 0 264.58 264.58"
   version="1.1"
   id="svg890"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:svg="http://www.w3.org/2000/svg">
  <defs
     id="defs2" />
  <g
     id="layer1"
     style="fill:#00be5a;fill-opacity:1">
    <g
       id="g37902"
       style="fill:#00be5a;fill-opacity:1">
      <circle
         style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:6.61449;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
         id="path14082"
         cx="132.28712"
         cy="132.28716"
         r="16.536238" />
      <g
         id="g31972"
         style="fill:#00be5a;fill-opacity:1">
        <path
           style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.55391;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
           id="path15079-9-6-2"
           transform="rotate(150)"
           d="m -48.790388,-278.76829 c 1.375733,0.38008 3.778011,1.69906 6.747005,3.78375 0.413023,0.29 0.847811,0.60231 1.300374,0.93557 1.26955,0.93489 2.876917,2.21115 4.689357,4.04813 1.745816,1.76945 3.714063,4.08962 5.797099,7.198 1.753533,2.61668 3.582194,5.78446 5.400025,9.60883 3.149699,6.62638 6.227597,15.15117 8.70143,25.7326 2.21665,9.48138 3.907031,20.45533 4.684311,32.68964 1.4914398,23.47515 -0.715185,46.65785 -5.805647,65.7865 -2.588599,9.7273 -5.814892,17.99733 -9.417093,24.59797 -1.890488,3.46411 -3.853175,6.411905 -5.843267,8.866476 -2.104906,2.59618 -4.204339,4.596491 -6.245487,6.08719 -2.051741,1.498435 -4.060366,2.504859 -6.005794,3.069223 -1.840769,0.534001 -3.662431,0.683925 -5.481457,0.45655 -1.764571,-0.220567 -3.612136,-0.807338 -5.533666,-1.846961 -1.887895,-1.021425 -3.846757,-2.480677 -5.834665,-4.444432 -3.952422,-3.904402 -7.83097,-9.618406 -11.31471,-17.156686 -3.524148,-7.62572 -6.50198,-16.80959 -8.748861,-27.26278 -5.091735,-23.68835 -5.202376,-47.76946 -3.204601,-66.67525 1.364944,-12.91704 3.647167,-23.1336 5.78832,-30.55392 0.583592,-2.02248 1.156821,-3.8388 1.698441,-5.4505 3.450532,-10.26773 6.988836,-16.35101 8.387316,-18.66612 l -8.466458,-5.11429 c -1.655846,2.74117 -5.553619,9.4909 -9.296835,20.62956 -0.584111,1.73813 -1.201238,3.68757 -1.828899,5.84963 -2.304715,7.93888 -4.738337,18.71499 -6.211046,32.24007 -2.147162,19.71914 -2.129523,44.90359 3.184464,69.86869 2.349584,11.03835 5.524422,20.97173 9.420182,29.41818 3.831707,8.307568 8.350964,15.153101 13.430329,20.144979 2.576889,2.532504 5.312866,4.602366 8.188816,6.136377 2.947667,1.572265 6.016664,2.567182 9.155763,2.925666 3.252611,0.371448 6.458413,0.04557 9.547901,-0.90012 3.229096,-0.988431 6.247863,-2.627023 9.037488,-4.737875 2.777282,-2.101511 5.433965,-4.740086 7.95145,-7.905101 2.380537,-2.992843 4.634422,-6.453877 6.740464,-10.363173 4.002689,-7.429913 7.455191,-16.452953 10.1671393,-26.762323 5.3252774,-20.24384 7.54173605,-44.48566 5.8242559,-68.86016 -0.8971905,-12.73294 -2.7776961,-24.20959 -5.2759074,-34.17451 -2.7874038,-11.11846 -6.2980878,-20.1928 -10.0406478,-27.31206 -2.161419,-4.11154 -4.38578,-7.54655 -6.591639,-10.38984 -2.624708,-3.38317 -5.195626,-5.89373 -7.582928,-7.73618 -2.49073,-1.92226 -4.749815,-3.09263 -6.610334,-3.77729 -0.658039,-0.24215 -1.292684,-0.42971 -1.89931,-0.57217 -4.36639,-1.02538 -7.442495,0.25932 -8.602678,0.58816 z" />
        <g
           id="g27478"
           transform="translate(-4.8406107,-166.8366)"
           style="fill:#00be5a;fill-opacity:1">
          <g
             id="g2188-2-9-4-3"
             style="fill:#00be5a;fill-opacity:1"
             transform="matrix(6.9641189e-8,14.644994,-19.336741,9.1951804e-8,300.32226,141.46704)">
            <path
               style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
               d="m 16.13938,5.3266 h 0.26458 c 0.12354,0 0.247406,0.06176 0.344383,0.1264118 0.05591,0.046865 0.141622,0.084894 0.173603,0.1006421 0.0127,0.003 0.01117,0.00253 0.01117,0.00253 0,0 8.03e-4,0.00145 -0.003,-0.011157 -0.01774,-0.0315 -0.07352,-0.1135646 -0.142944,-0.1502587 C 16.685714,5.3271298 16.545,5.2566 16.40396,5.2566 h -0.26458 z"
               id="path1101-6-9-2-83" />
            <path
               style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
               d="m 16.13938,5.2816 h 0.26458 c 0.14104,0 0.281754,-0.07053 0.383212,-0.1381682 0.06942,-0.036694 0.125201,-0.118759 0.142971,-0.1502545 0.0038,-0.012612 0.003,-0.011157 0.003,-0.011157 0,0 0.0015,-4.753e-4 -0.01117,0.00253 -0.03198,0.015748 -0.117689,0.053778 -0.173603,0.1006421 C 16.651366,5.1498398 16.5275,5.2116 16.40396,5.2116 h -0.26458 z"
               id="path1101-4-7-6-2-33" />
          </g>
          <path
             id="rect3698-3-3-9-6-5-0-2-8"
             style="vector-effect:none;fill:#00be5a;fill-opacity:1;fill-rule:evenodd;stroke-width:3.91082;stroke-linejoin:bevel;stop-color:#000000"
             d="m 207.28366,355.53032 -3.38322,20.9263 c -0.309,1.91125 -1.95584,3.48772 -4.38527,3.48772 h -2.00402 c -2.42941,0 -4.07625,-1.57647 -4.38525,-3.48772 l -3.38322,-20.9263 c -0.309,-1.91125 1.95582,-3.48772 4.38525,-3.48772 h 8.77048 c 2.42943,0 4.69425,1.57646 4.38525,3.48772 z" />
          <circle
             style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path22550-4-0-0"
             cx="204.2769"
             cy="366.4433"
             r="2.0954933" />
          <circle
             style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path22550-4-2-5-4"
             cx="192.59309"
             cy="366.4433"
             r="2.0954933" />
        </g>
      </g>
      <g
         id="g31972-9"
         transform="rotate(120,132.28991,132.29016)"
         style="fill:#00be5a;fill-opacity:1">
        <path
           style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.55391;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
           id="path15079-9-6-2-0"
           transform="rotate(150)"
           d="m -48.790388,-278.76829 c 1.375733,0.38008 3.778011,1.69906 6.747005,3.78375 0.413023,0.29 0.847811,0.60231 1.300374,0.93557 1.26955,0.93489 2.876917,2.21115 4.689357,4.04813 1.745816,1.76945 3.714063,4.08962 5.797099,7.198 1.753533,2.61668 3.582194,5.78446 5.400025,9.60883 3.149699,6.62638 6.227597,15.15117 8.70143,25.7326 2.21665,9.48138 3.907031,20.45533 4.684311,32.68964 1.4914398,23.47515 -0.715185,46.65785 -5.805647,65.7865 -2.588599,9.7273 -5.814892,17.99733 -9.417093,24.59797 -1.890488,3.46411 -3.853175,6.411905 -5.843267,8.866476 -2.104906,2.59618 -4.204339,4.596491 -6.245487,6.08719 -2.051741,1.498435 -4.060366,2.504859 -6.005794,3.069223 -1.840769,0.534001 -3.662431,0.683925 -5.481457,0.45655 -1.764571,-0.220567 -3.612136,-0.807338 -5.533666,-1.846961 -1.887895,-1.021425 -3.846757,-2.480677 -5.834665,-4.444432 -3.952422,-3.904402 -7.83097,-9.618406 -11.31471,-17.156686 -3.524148,-7.62572 -6.50198,-16.80959 -8.748861,-27.26278 -5.091735,-23.68835 -5.202376,-47.76946 -3.204601,-66.67525 1.364944,-12.91704 3.647167,-23.1336 5.78832,-30.55392 0.583592,-2.02248 1.156821,-3.8388 1.698441,-5.4505 3.450532,-10.26773 6.988836,-16.35101 8.387316,-18.66612 l -8.466458,-5.11429 c -1.655846,2.74117 -5.553619,9.4909 -9.296835,20.62956 -0.584111,1.73813 -1.201238,3.68757 -1.828899,5.84963 -2.304715,7.93888 -4.738337,18.71499 -6.211046,32.24007 -2.147162,19.71914 -2.129523,44.90359 3.184464,69.86869 2.349584,11.03835 5.524422,20.97173 9.420182,29.41818 3.831707,8.307568 8.350964,15.153101 13.430329,20.144979 2.576889,2.532504 5.312866,4.602366 8.188816,6.136377 2.947667,1.572265 6.016664,2.567182 9.155763,2.925666 3.252611,0.371448 6.458413,0.04557 9.547901,-0.90012 3.229096,-0.988431 6.247863,-2.627023 9.037488,-4.737875 2.777282,-2.101511 5.433965,-4.740086 7.95145,-7.905101 2.380537,-2.992843 4.634422,-6.453877 6.740464,-10.363173 4.002689,-7.429913 7.455191,-16.452953 10.1671393,-26.762323 5.3252774,-20.24384 7.54173605,-44.48566 5.8242559,-68.86016 -0.8971905,-12.73294 -2.7776961,-24.20959 -5.2759074,-34.17451 -2.7874038,-11.11846 -6.2980878,-20.1928 -10.0406478,-27.31206 -2.161419,-4.11154 -4.38578,-7.54655 -6.591639,-10.38984 -2.624708,-3.38317 -5.195626,-5.89373 -7.582928,-7.73618 -2.49073,-1.92226 -4.749815,-3.09263 -6.610334,-3.77729 -0.658039,-0.24215 -1.292684,-0.42971 -1.89931,-0.57217 -4.36639,-1.02538 -7.442495,0.25932 -8.602678,0.58816 z" />
        <g
           id="g27478-6"
           transform="translate(-4.8406107,-166.8366)"
           style="fill:#00be5a;fill-opacity:1">
          <g
             id="g2188-2-9-4-3-8"
             style="fill:#00be5a;fill-opacity:1"
             transform="matrix(6.9641189e-8,14.644994,-19.336741,9.1951804e-8,300.32226,141.46704)">
            <path
               style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
               d="m 16.13938,5.3266 h 0.26458 c 0.12354,0 0.247406,0.06176 0.344383,0.1264118 0.05591,0.046865 0.141622,0.084894 0.173603,0.1006421 0.0127,0.003 0.01117,0.00253 0.01117,0.00253 0,0 8.03e-4,0.00145 -0.003,-0.011157 -0.01774,-0.0315 -0.07352,-0.1135646 -0.142944,-0.1502587 C 16.685714,5.3271298 16.545,5.2566 16.40396,5.2566 h -0.26458 z"
               id="path1101-6-9-2-83-7" />
            <path
               style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
               d="m 16.13938,5.2816 h 0.26458 c 0.14104,0 0.281754,-0.07053 0.383212,-0.1381682 0.06942,-0.036694 0.125201,-0.118759 0.142971,-0.1502545 0.0038,-0.012612 0.003,-0.011157 0.003,-0.011157 0,0 0.0015,-4.753e-4 -0.01117,0.00253 -0.03198,0.015748 -0.117689,0.053778 -0.173603,0.1006421 C 16.651366,5.1498398 16.5275,5.2116 16.40396,5.2116 h -0.26458 z"
               id="path1101-4-7-6-2-33-9" />
          </g>
          <path
             id="rect3698-3-3-9-6-5-0-2-8-0"
             style="vector-effect:none;fill:#00be5a;fill-opacity:1;fill-rule:evenodd;stroke-width:3.91082;stroke-linejoin:bevel;stop-color:#000000"
             d="m 207.28366,355.53032 -3.38322,20.9263 c -0.309,1.91125 -1.95584,3.48772 -4.38527,3.48772 h -2.00402 c -2.42941,0 -4.07625,-1.57647 -4.38525,-3.48772 l -3.38322,-20.9263 c -0.309,-1.91125 1.95582,-3.48772 4.38525,-3.48772 h 8.77048 c 2.42943,0 4.69425,1.57646 4.38525,3.48772 z" />
          <circle
             style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path22550-4-0-0-3"
             cx="204.2769"
             cy="366.4433"
             r="2.0954933" />
          <circle
             style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path22550-4-2-5-4-3"
             cx="192.59309"
             cy="366.4433"
             r="2.0954933" />
        </g>
      </g>
      <g
         id="g31972-2"
         transform="rotate(-120,132.29009,132.29016)"
         style="fill:#00be5a;fill-opacity:1">
        <path
           style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.55391;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
           id="path15079-9-6-2-6"
           transform="rotate(150)"
           d="m -48.790388,-278.76829 c 1.375733,0.38008 3.778011,1.69906 6.747005,3.78375 0.413023,0.29 0.847811,0.60231 1.300374,0.93557 1.26955,0.93489 2.876917,2.21115 4.689357,4.04813 1.745816,1.76945 3.714063,4.08962 5.797099,7.198 1.753533,2.61668 3.582194,5.78446 5.400025,9.60883 3.149699,6.62638 6.227597,15.15117 8.70143,25.7326 2.21665,9.48138 3.907031,20.45533 4.684311,32.68964 1.4914398,23.47515 -0.715185,46.65785 -5.805647,65.7865 -2.588599,9.7273 -5.814892,17.99733 -9.417093,24.59797 -1.890488,3.46411 -3.853175,6.411905 -5.843267,8.866476 -2.104906,2.59618 -4.204339,4.596491 -6.245487,6.08719 -2.051741,1.498435 -4.060366,2.504859 -6.005794,3.069223 -1.840769,0.534001 -3.662431,0.683925 -5.481457,0.45655 -1.764571,-0.220567 -3.612136,-0.807338 -5.533666,-1.846961 -1.887895,-1.021425 -3.846757,-2.480677 -5.834665,-4.444432 -3.952422,-3.904402 -7.83097,-9.618406 -11.31471,-17.156686 -3.524148,-7.62572 -6.50198,-16.80959 -8.748861,-27.26278 -5.091735,-23.68835 -5.202376,-47.76946 -3.204601,-66.67525 1.364944,-12.91704 3.647167,-23.1336 5.78832,-30.55392 0.583592,-2.02248 1.156821,-3.8388 1.698441,-5.4505 3.450532,-10.26773 6.988836,-16.35101 8.387316,-18.66612 l -8.466458,-5.11429 c -1.655846,2.74117 -5.553619,9.4909 -9.296835,20.62956 -0.584111,1.73813 -1.201238,3.68757 -1.828899,5.84963 -2.304715,7.93888 -4.738337,18.71499 -6.211046,32.24007 -2.147162,19.71914 -2.129523,44.90359 3.184464,69.86869 2.349584,11.03835 5.524422,20.97173 9.420182,29.41818 3.831707,8.307568 8.350964,15.153101 13.430329,20.144979 2.576889,2.532504 5.312866,4.602366 8.188816,6.136377 2.947667,1.572265 6.016664,2.567182 9.155763,2.925666 3.252611,0.371448 6.458413,0.04557 9.547901,-0.90012 3.229096,-0.988431 6.247863,-2.627023 9.037488,-4.737875 2.777282,-2.101511 5.433965,-4.740086 7.95145,-7.905101 2.380537,-2.992843 4.634422,-6.453877 6.740464,-10.363173 4.002689,-7.429913 7.455191,-16.452953 10.1671393,-26.762323 5.3252774,-20.24384 7.54173605,-44.48566 5.8242559,-68.86016 -0.8971905,-12.73294 -2.7776961,-24.20959 -5.2759074,-34.17451 -2.7874038,-11.11846 -6.2980878,-20.1928 -10.0406478,-27.31206 -2.161419,-4.11154 -4.38578,-7.54655 -6.591639,-10.38984 -2.624708,-3.38317 -5.195626,-5.89373 -7.582928,-7.73618 -2.49073,-1.92226 -4.749815,-3.09263 -6.610334,-3.77729 -0.658039,-0.24215 -1.292684,-0.42971 -1.89931,-0.57217 -4.36639,-1.02538 -7.442495,0.25932 -8.602678,0.58816 z" />
        <g
           id="g27478-5"
           transform="translate(-4.8406107,-166.8366)"
           style="fill:#00be5a;fill-opacity:1">
          <g
             id="g2188-2-9-4-3-2"
             style="fill:#00be5a;fill-opacity:1"
             transform="matrix(6.9641189e-8,14.644994,-19.336741,9.1951804e-8,300.32226,141.46704)">
            <path
               style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
               d="m 16.13938,5.3266 h 0.26458 c 0.12354,0 0.247406,0.06176 0.344383,0.1264118 0.05591,0.046865 0.141622,0.084894 0.173603,0.1006421 0.0127,0.003 0.01117,0.00253 0.01117,0.00253 0,0 8.03e-4,0.00145 -0.003,-0.011157 -0.01774,-0.0315 -0.07352,-0.1135646 -0.142944,-0.1502587 C 16.685714,5.3271298 16.545,5.2566 16.40396,5.2566 h -0.26458 z"
               id="path1101-6-9-2-83-6" />
            <path
               style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
               d="m 16.13938,5.2816 h 0.26458 c 0.14104,0 0.281754,-0.07053 0.383212,-0.1381682 0.06942,-0.036694 0.125201,-0.118759 0.142971,-0.1502545 0.0038,-0.012612 0.003,-0.011157 0.003,-0.011157 0,0 0.0015,-4.753e-4 -0.01117,0.00253 -0.03198,0.015748 -0.117689,0.053778 -0.173603,0.1006421 C 16.651366,5.1498398 16.5275,5.2116 16.40396,5.2116 h -0.26458 z"
               id="path1101-4-7-6-2-33-5" />
          </g>
          <path
             id="rect3698-3-3-9-6-5-0-2-8-8"
             style="vector-effect:none;fill:#00be5a;fill-opacity:1;fill-rule:evenodd;stroke-width:3.91082;stroke-linejoin:bevel;stop-color:#000000"
             d="m 207.28366,355.53032 -3.38322,20.9263 c -0.309,1.91125 -1.95584,3.48772 -4.38527,3.48772 h -2.00402 c -2.42941,0 -4.07625,-1.57647 -4.38525,-3.48772 l -3.38322,-20.9263 c -0.309,-1.91125 1.95582,-3.48772 4.38525,-3.48772 h 8.77048 c 2.42943,0 4.69425,1.57646 4.38525,3.48772 z" />
          <circle
             style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path22550-4-0-0-7"
             cx="204.2769"
             cy="366.4433"
             r="2.0954933" />
          <circle
             style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path22550-4-2-5-4-9"
             cx="192.59309"
             cy="366.4433"
             r="2.0954933" />
        </g>
      </g>
    </g>
  </g>
</svg>

branding/svg/reactpy-logo-landscape.svg METASEP
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
   width="2500"
   height="755"
   viewBox="0 0 661.45 199.7579"
   version="1.1"
   id="svg890"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:svg="http://www.w3.org/2000/svg">
  <defs
     id="defs2" />
  <g
     id="layer1"
     style="fill:#00be5a;fill-opacity:1"
     transform="matrix(1.0094097,0,0,1.0094097,-20.83039,-28.982858)">
    <g
       id="g37902"
       style="fill:#00be5a;fill-opacity:1">
      <circle
         style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:6.61449;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
         id="path14082"
         cx="132.28712"
         cy="132.28716"
         r="16.536238" />
      <g
         id="g31972"
         style="fill:#00be5a;fill-opacity:1">
        <path
           style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.55391;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
           id="path15079-9-6-2"
           transform="rotate(150)"
           d="m -48.790388,-278.76829 c 1.375733,0.38008 3.778011,1.69906 6.747005,3.78375 0.413023,0.29 0.847811,0.60231 1.300374,0.93557 1.26955,0.93489 2.876917,2.21115 4.689357,4.04813 1.745816,1.76945 3.714063,4.08962 5.797099,7.198 1.753533,2.61668 3.582194,5.78446 5.400025,9.60883 3.149699,6.62638 6.227597,15.15117 8.70143,25.7326 2.21665,9.48138 3.907031,20.45533 4.684311,32.68964 1.4914398,23.47515 -0.715185,46.65785 -5.805647,65.7865 -2.588599,9.7273 -5.814892,17.99733 -9.417093,24.59797 -1.890488,3.46411 -3.853175,6.411905 -5.843267,8.866476 -2.104906,2.59618 -4.204339,4.596491 -6.245487,6.08719 -2.051741,1.498435 -4.060366,2.504859 -6.005794,3.069223 -1.840769,0.534001 -3.662431,0.683925 -5.481457,0.45655 -1.764571,-0.220567 -3.612136,-0.807338 -5.533666,-1.846961 -1.887895,-1.021425 -3.846757,-2.480677 -5.834665,-4.444432 -3.952422,-3.904402 -7.83097,-9.618406 -11.31471,-17.156686 -3.524148,-7.62572 -6.50198,-16.80959 -8.748861,-27.26278 -5.091735,-23.68835 -5.202376,-47.76946 -3.204601,-66.67525 1.364944,-12.91704 3.647167,-23.1336 5.78832,-30.55392 0.583592,-2.02248 1.156821,-3.8388 1.698441,-5.4505 3.450532,-10.26773 6.988836,-16.35101 8.387316,-18.66612 l -8.466458,-5.11429 c -1.655846,2.74117 -5.553619,9.4909 -9.296835,20.62956 -0.584111,1.73813 -1.201238,3.68757 -1.828899,5.84963 -2.304715,7.93888 -4.738337,18.71499 -6.211046,32.24007 -2.147162,19.71914 -2.129523,44.90359 3.184464,69.86869 2.349584,11.03835 5.524422,20.97173 9.420182,29.41818 3.831707,8.307568 8.350964,15.153101 13.430329,20.144979 2.576889,2.532504 5.312866,4.602366 8.188816,6.136377 2.947667,1.572265 6.016664,2.567182 9.155763,2.925666 3.252611,0.371448 6.458413,0.04557 9.547901,-0.90012 3.229096,-0.988431 6.247863,-2.627023 9.037488,-4.737875 2.777282,-2.101511 5.433965,-4.740086 7.95145,-7.905101 2.380537,-2.992843 4.634422,-6.453877 6.740464,-10.363173 4.002689,-7.429913 7.455191,-16.452953 10.1671393,-26.762323 5.3252774,-20.24384 7.54173605,-44.48566 5.8242559,-68.86016 -0.8971905,-12.73294 -2.7776961,-24.20959 -5.2759074,-34.17451 -2.7874038,-11.11846 -6.2980878,-20.1928 -10.0406478,-27.31206 -2.161419,-4.11154 -4.38578,-7.54655 -6.591639,-10.38984 -2.624708,-3.38317 -5.195626,-5.89373 -7.582928,-7.73618 -2.49073,-1.92226 -4.749815,-3.09263 -6.610334,-3.77729 -0.658039,-0.24215 -1.292684,-0.42971 -1.89931,-0.57217 -4.36639,-1.02538 -7.442495,0.25932 -8.602678,0.58816 z" />
        <g
           id="g27478"
           transform="translate(-4.8406107,-166.8366)"
           style="fill:#00be5a;fill-opacity:1">
          <g
             id="g2188-2-9-4-3"
             style="fill:#00be5a;fill-opacity:1"
             transform="matrix(6.9641189e-8,14.644994,-19.336741,9.1951804e-8,300.32226,141.46704)">
            <path
               style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
               d="m 16.13938,5.3266 h 0.26458 c 0.12354,0 0.247406,0.06176 0.344383,0.1264118 0.05591,0.046865 0.141622,0.084894 0.173603,0.1006421 0.0127,0.003 0.01117,0.00253 0.01117,0.00253 0,0 8.03e-4,0.00145 -0.003,-0.011157 -0.01774,-0.0315 -0.07352,-0.1135646 -0.142944,-0.1502587 C 16.685714,5.3271298 16.545,5.2566 16.40396,5.2566 h -0.26458 z"
               id="path1101-6-9-2-83" />
            <path
               style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
               d="m 16.13938,5.2816 h 0.26458 c 0.14104,0 0.281754,-0.07053 0.383212,-0.1381682 0.06942,-0.036694 0.125201,-0.118759 0.142971,-0.1502545 0.0038,-0.012612 0.003,-0.011157 0.003,-0.011157 0,0 0.0015,-4.753e-4 -0.01117,0.00253 -0.03198,0.015748 -0.117689,0.053778 -0.173603,0.1006421 C 16.651366,5.1498398 16.5275,5.2116 16.40396,5.2116 h -0.26458 z"
               id="path1101-4-7-6-2-33" />
          </g>
          <path
             id="rect3698-3-3-9-6-5-0-2-8"
             style="vector-effect:none;fill:#00be5a;fill-opacity:1;fill-rule:evenodd;stroke-width:3.91082;stroke-linejoin:bevel;stop-color:#000000"
             d="m 207.28366,355.53032 -3.38322,20.9263 c -0.309,1.91125 -1.95584,3.48772 -4.38527,3.48772 h -2.00402 c -2.42941,0 -4.07625,-1.57647 -4.38525,-3.48772 l -3.38322,-20.9263 c -0.309,-1.91125 1.95582,-3.48772 4.38525,-3.48772 h 8.77048 c 2.42943,0 4.69425,1.57646 4.38525,3.48772 z" />
          <circle
             style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path22550-4-0-0"
             cx="204.2769"
             cy="366.4433"
             r="2.0954933" />
          <circle
             style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path22550-4-2-5-4"
             cx="192.59309"
             cy="366.4433"
             r="2.0954933" />
        </g>
      </g>
      <g
         id="g31972-9"
         transform="rotate(120,132.28991,132.29016)"
         style="fill:#00be5a;fill-opacity:1">
        <path
           style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.55391;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
           id="path15079-9-6-2-0"
           transform="rotate(150)"
           d="m -48.790388,-278.76829 c 1.375733,0.38008 3.778011,1.69906 6.747005,3.78375 0.413023,0.29 0.847811,0.60231 1.300374,0.93557 1.26955,0.93489 2.876917,2.21115 4.689357,4.04813 1.745816,1.76945 3.714063,4.08962 5.797099,7.198 1.753533,2.61668 3.582194,5.78446 5.400025,9.60883 3.149699,6.62638 6.227597,15.15117 8.70143,25.7326 2.21665,9.48138 3.907031,20.45533 4.684311,32.68964 1.4914398,23.47515 -0.715185,46.65785 -5.805647,65.7865 -2.588599,9.7273 -5.814892,17.99733 -9.417093,24.59797 -1.890488,3.46411 -3.853175,6.411905 -5.843267,8.866476 -2.104906,2.59618 -4.204339,4.596491 -6.245487,6.08719 -2.051741,1.498435 -4.060366,2.504859 -6.005794,3.069223 -1.840769,0.534001 -3.662431,0.683925 -5.481457,0.45655 -1.764571,-0.220567 -3.612136,-0.807338 -5.533666,-1.846961 -1.887895,-1.021425 -3.846757,-2.480677 -5.834665,-4.444432 -3.952422,-3.904402 -7.83097,-9.618406 -11.31471,-17.156686 -3.524148,-7.62572 -6.50198,-16.80959 -8.748861,-27.26278 -5.091735,-23.68835 -5.202376,-47.76946 -3.204601,-66.67525 1.364944,-12.91704 3.647167,-23.1336 5.78832,-30.55392 0.583592,-2.02248 1.156821,-3.8388 1.698441,-5.4505 3.450532,-10.26773 6.988836,-16.35101 8.387316,-18.66612 l -8.466458,-5.11429 c -1.655846,2.74117 -5.553619,9.4909 -9.296835,20.62956 -0.584111,1.73813 -1.201238,3.68757 -1.828899,5.84963 -2.304715,7.93888 -4.738337,18.71499 -6.211046,32.24007 -2.147162,19.71914 -2.129523,44.90359 3.184464,69.86869 2.349584,11.03835 5.524422,20.97173 9.420182,29.41818 3.831707,8.307568 8.350964,15.153101 13.430329,20.144979 2.576889,2.532504 5.312866,4.602366 8.188816,6.136377 2.947667,1.572265 6.016664,2.567182 9.155763,2.925666 3.252611,0.371448 6.458413,0.04557 9.547901,-0.90012 3.229096,-0.988431 6.247863,-2.627023 9.037488,-4.737875 2.777282,-2.101511 5.433965,-4.740086 7.95145,-7.905101 2.380537,-2.992843 4.634422,-6.453877 6.740464,-10.363173 4.002689,-7.429913 7.455191,-16.452953 10.1671393,-26.762323 5.3252774,-20.24384 7.54173605,-44.48566 5.8242559,-68.86016 -0.8971905,-12.73294 -2.7776961,-24.20959 -5.2759074,-34.17451 -2.7874038,-11.11846 -6.2980878,-20.1928 -10.0406478,-27.31206 -2.161419,-4.11154 -4.38578,-7.54655 -6.591639,-10.38984 -2.624708,-3.38317 -5.195626,-5.89373 -7.582928,-7.73618 -2.49073,-1.92226 -4.749815,-3.09263 -6.610334,-3.77729 -0.658039,-0.24215 -1.292684,-0.42971 -1.89931,-0.57217 -4.36639,-1.02538 -7.442495,0.25932 -8.602678,0.58816 z" />
        <g
           id="g27478-6"
           transform="translate(-4.8406107,-166.8366)"
           style="fill:#00be5a;fill-opacity:1">
          <g
             id="g2188-2-9-4-3-8"
             style="fill:#00be5a;fill-opacity:1"
             transform="matrix(6.9641189e-8,14.644994,-19.336741,9.1951804e-8,300.32226,141.46704)">
            <path
               style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
               d="m 16.13938,5.3266 h 0.26458 c 0.12354,0 0.247406,0.06176 0.344383,0.1264118 0.05591,0.046865 0.141622,0.084894 0.173603,0.1006421 0.0127,0.003 0.01117,0.00253 0.01117,0.00253 0,0 8.03e-4,0.00145 -0.003,-0.011157 -0.01774,-0.0315 -0.07352,-0.1135646 -0.142944,-0.1502587 C 16.685714,5.3271298 16.545,5.2566 16.40396,5.2566 h -0.26458 z"
               id="path1101-6-9-2-83-7" />
            <path
               style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
               d="m 16.13938,5.2816 h 0.26458 c 0.14104,0 0.281754,-0.07053 0.383212,-0.1381682 0.06942,-0.036694 0.125201,-0.118759 0.142971,-0.1502545 0.0038,-0.012612 0.003,-0.011157 0.003,-0.011157 0,0 0.0015,-4.753e-4 -0.01117,0.00253 -0.03198,0.015748 -0.117689,0.053778 -0.173603,0.1006421 C 16.651366,5.1498398 16.5275,5.2116 16.40396,5.2116 h -0.26458 z"
               id="path1101-4-7-6-2-33-9" />
          </g>
          <path
             id="rect3698-3-3-9-6-5-0-2-8-0"
             style="vector-effect:none;fill:#00be5a;fill-opacity:1;fill-rule:evenodd;stroke-width:3.91082;stroke-linejoin:bevel;stop-color:#000000"
             d="m 207.28366,355.53032 -3.38322,20.9263 c -0.309,1.91125 -1.95584,3.48772 -4.38527,3.48772 h -2.00402 c -2.42941,0 -4.07625,-1.57647 -4.38525,-3.48772 l -3.38322,-20.9263 c -0.309,-1.91125 1.95582,-3.48772 4.38525,-3.48772 h 8.77048 c 2.42943,0 4.69425,1.57646 4.38525,3.48772 z" />
          <circle
             style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path22550-4-0-0-3"
             cx="204.2769"
             cy="366.4433"
             r="2.0954933" />
          <circle
             style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path22550-4-2-5-4-3"
             cx="192.59309"
             cy="366.4433"
             r="2.0954933" />
        </g>
      </g>
      <g
         id="g31972-2"
         transform="rotate(-120,132.29009,132.29016)"
         style="fill:#00be5a;fill-opacity:1">
        <path
           style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.55391;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
           id="path15079-9-6-2-6"
           transform="rotate(150)"
           d="m -48.790388,-278.76829 c 1.375733,0.38008 3.778011,1.69906 6.747005,3.78375 0.413023,0.29 0.847811,0.60231 1.300374,0.93557 1.26955,0.93489 2.876917,2.21115 4.689357,4.04813 1.745816,1.76945 3.714063,4.08962 5.797099,7.198 1.753533,2.61668 3.582194,5.78446 5.400025,9.60883 3.149699,6.62638 6.227597,15.15117 8.70143,25.7326 2.21665,9.48138 3.907031,20.45533 4.684311,32.68964 1.4914398,23.47515 -0.715185,46.65785 -5.805647,65.7865 -2.588599,9.7273 -5.814892,17.99733 -9.417093,24.59797 -1.890488,3.46411 -3.853175,6.411905 -5.843267,8.866476 -2.104906,2.59618 -4.204339,4.596491 -6.245487,6.08719 -2.051741,1.498435 -4.060366,2.504859 -6.005794,3.069223 -1.840769,0.534001 -3.662431,0.683925 -5.481457,0.45655 -1.764571,-0.220567 -3.612136,-0.807338 -5.533666,-1.846961 -1.887895,-1.021425 -3.846757,-2.480677 -5.834665,-4.444432 -3.952422,-3.904402 -7.83097,-9.618406 -11.31471,-17.156686 -3.524148,-7.62572 -6.50198,-16.80959 -8.748861,-27.26278 -5.091735,-23.68835 -5.202376,-47.76946 -3.204601,-66.67525 1.364944,-12.91704 3.647167,-23.1336 5.78832,-30.55392 0.583592,-2.02248 1.156821,-3.8388 1.698441,-5.4505 3.450532,-10.26773 6.988836,-16.35101 8.387316,-18.66612 l -8.466458,-5.11429 c -1.655846,2.74117 -5.553619,9.4909 -9.296835,20.62956 -0.584111,1.73813 -1.201238,3.68757 -1.828899,5.84963 -2.304715,7.93888 -4.738337,18.71499 -6.211046,32.24007 -2.147162,19.71914 -2.129523,44.90359 3.184464,69.86869 2.349584,11.03835 5.524422,20.97173 9.420182,29.41818 3.831707,8.307568 8.350964,15.153101 13.430329,20.144979 2.576889,2.532504 5.312866,4.602366 8.188816,6.136377 2.947667,1.572265 6.016664,2.567182 9.155763,2.925666 3.252611,0.371448 6.458413,0.04557 9.547901,-0.90012 3.229096,-0.988431 6.247863,-2.627023 9.037488,-4.737875 2.777282,-2.101511 5.433965,-4.740086 7.95145,-7.905101 2.380537,-2.992843 4.634422,-6.453877 6.740464,-10.363173 4.002689,-7.429913 7.455191,-16.452953 10.1671393,-26.762323 5.3252774,-20.24384 7.54173605,-44.48566 5.8242559,-68.86016 -0.8971905,-12.73294 -2.7776961,-24.20959 -5.2759074,-34.17451 -2.7874038,-11.11846 -6.2980878,-20.1928 -10.0406478,-27.31206 -2.161419,-4.11154 -4.38578,-7.54655 -6.591639,-10.38984 -2.624708,-3.38317 -5.195626,-5.89373 -7.582928,-7.73618 -2.49073,-1.92226 -4.749815,-3.09263 -6.610334,-3.77729 -0.658039,-0.24215 -1.292684,-0.42971 -1.89931,-0.57217 -4.36639,-1.02538 -7.442495,0.25932 -8.602678,0.58816 z" />
        <g
           id="g27478-5"
           transform="translate(-4.8406107,-166.8366)"
           style="fill:#00be5a;fill-opacity:1">
          <g
             id="g2188-2-9-4-3-2"
             style="fill:#00be5a;fill-opacity:1"
             transform="matrix(6.9641189e-8,14.644994,-19.336741,9.1951804e-8,300.32226,141.46704)">
            <path
               style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
               d="m 16.13938,5.3266 h 0.26458 c 0.12354,0 0.247406,0.06176 0.344383,0.1264118 0.05591,0.046865 0.141622,0.084894 0.173603,0.1006421 0.0127,0.003 0.01117,0.00253 0.01117,0.00253 0,0 8.03e-4,0.00145 -0.003,-0.011157 -0.01774,-0.0315 -0.07352,-0.1135646 -0.142944,-0.1502587 C 16.685714,5.3271298 16.545,5.2566 16.40396,5.2566 h -0.26458 z"
               id="path1101-6-9-2-83-6" />
            <path
               style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
               d="m 16.13938,5.2816 h 0.26458 c 0.14104,0 0.281754,-0.07053 0.383212,-0.1381682 0.06942,-0.036694 0.125201,-0.118759 0.142971,-0.1502545 0.0038,-0.012612 0.003,-0.011157 0.003,-0.011157 0,0 0.0015,-4.753e-4 -0.01117,0.00253 -0.03198,0.015748 -0.117689,0.053778 -0.173603,0.1006421 C 16.651366,5.1498398 16.5275,5.2116 16.40396,5.2116 h -0.26458 z"
               id="path1101-4-7-6-2-33-5" />
          </g>
          <path
             id="rect3698-3-3-9-6-5-0-2-8-8"
             style="vector-effect:none;fill:#00be5a;fill-opacity:1;fill-rule:evenodd;stroke-width:3.91082;stroke-linejoin:bevel;stop-color:#000000"
             d="m 207.28366,355.53032 -3.38322,20.9263 c -0.309,1.91125 -1.95584,3.48772 -4.38527,3.48772 h -2.00402 c -2.42941,0 -4.07625,-1.57647 -4.38525,-3.48772 l -3.38322,-20.9263 c -0.309,-1.91125 1.95582,-3.48772 4.38525,-3.48772 h 8.77048 c 2.42943,0 4.69425,1.57646 4.38525,3.48772 z" />
          <circle
             style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path22550-4-0-0-7"
             cx="204.2769"
             cy="366.4433"
             r="2.0954933" />
          <circle
             style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path22550-4-2-5-4-9"
             cx="192.59309"
             cy="366.4433"
             r="2.0954933" />
        </g>
      </g>
    </g>
  </g>
  <text
     xml:space="preserve"
     style="font-weight:bold;font-size:106.828px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';letter-spacing:0px;word-spacing:0px;fill:#00be5a;fill-opacity:1;stroke-width:0.26707"
     x="235.75421"
     y="142.73428"
     id="text5299"><tspan
       id="tspan5297"
       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:106.828px;font-family:sans-serif;-inkscape-font-specification:sans-serif;fill:#00be5a;fill-opacity:1;stroke-width:0.26707"
       x="235.75421"
       y="142.73428">ReactPy</tspan></text>
</svg>

branding/svg/reactpy-logo-landscape-padded.svg METASEP
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
   width="1080"
   height="566"
   viewBox="0 0 285.7464 149.75228"
   version="1.1"
   id="svg890"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:svg="http://www.w3.org/2000/svg">
  <defs
     id="defs2" />
  <g
     id="g1042"
     transform="matrix(0.87851903,0,0,0.87593471,7.3100546,5.459132)">
    <g
       id="layer1"
       style="fill:#00be5a;fill-opacity:1"
       transform="matrix(0.43660068,0,0,0.43660068,2.2503739,23.363441)">
      <g
         id="g37902"
         style="fill:#00be5a;fill-opacity:1">
        <circle
           style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:6.61449;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
           id="path14082"
           cx="132.28712"
           cy="132.28716"
           r="16.536238" />
        <g
           id="g31972"
           style="fill:#00be5a;fill-opacity:1">
          <path
             style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.55391;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path15079-9-6-2"
             transform="rotate(150)"
             d="m -48.790388,-278.76829 c 1.375733,0.38008 3.778011,1.69906 6.747005,3.78375 0.413023,0.29 0.847811,0.60231 1.300374,0.93557 1.26955,0.93489 2.876917,2.21115 4.689357,4.04813 1.745816,1.76945 3.714063,4.08962 5.797099,7.198 1.753533,2.61668 3.582194,5.78446 5.400025,9.60883 3.149699,6.62638 6.227597,15.15117 8.70143,25.7326 2.21665,9.48138 3.907031,20.45533 4.684311,32.68964 1.4914398,23.47515 -0.715185,46.65785 -5.805647,65.7865 -2.588599,9.7273 -5.814892,17.99733 -9.417093,24.59797 -1.890488,3.46411 -3.853175,6.411905 -5.843267,8.866476 -2.104906,2.59618 -4.204339,4.596491 -6.245487,6.08719 -2.051741,1.498435 -4.060366,2.504859 -6.005794,3.069223 -1.840769,0.534001 -3.662431,0.683925 -5.481457,0.45655 -1.764571,-0.220567 -3.612136,-0.807338 -5.533666,-1.846961 -1.887895,-1.021425 -3.846757,-2.480677 -5.834665,-4.444432 -3.952422,-3.904402 -7.83097,-9.618406 -11.31471,-17.156686 -3.524148,-7.62572 -6.50198,-16.80959 -8.748861,-27.26278 -5.091735,-23.68835 -5.202376,-47.76946 -3.204601,-66.67525 1.364944,-12.91704 3.647167,-23.1336 5.78832,-30.55392 0.583592,-2.02248 1.156821,-3.8388 1.698441,-5.4505 3.450532,-10.26773 6.988836,-16.35101 8.387316,-18.66612 l -8.466458,-5.11429 c -1.655846,2.74117 -5.553619,9.4909 -9.296835,20.62956 -0.584111,1.73813 -1.201238,3.68757 -1.828899,5.84963 -2.304715,7.93888 -4.738337,18.71499 -6.211046,32.24007 -2.147162,19.71914 -2.129523,44.90359 3.184464,69.86869 2.349584,11.03835 5.524422,20.97173 9.420182,29.41818 3.831707,8.307568 8.350964,15.153101 13.430329,20.144979 2.576889,2.532504 5.312866,4.602366 8.188816,6.136377 2.947667,1.572265 6.016664,2.567182 9.155763,2.925666 3.252611,0.371448 6.458413,0.04557 9.547901,-0.90012 3.229096,-0.988431 6.247863,-2.627023 9.037488,-4.737875 2.777282,-2.101511 5.433965,-4.740086 7.95145,-7.905101 2.380537,-2.992843 4.634422,-6.453877 6.740464,-10.363173 4.002689,-7.429913 7.455191,-16.452953 10.1671393,-26.762323 5.3252774,-20.24384 7.54173605,-44.48566 5.8242559,-68.86016 -0.8971905,-12.73294 -2.7776961,-24.20959 -5.2759074,-34.17451 -2.7874038,-11.11846 -6.2980878,-20.1928 -10.0406478,-27.31206 -2.161419,-4.11154 -4.38578,-7.54655 -6.591639,-10.38984 -2.624708,-3.38317 -5.195626,-5.89373 -7.582928,-7.73618 -2.49073,-1.92226 -4.749815,-3.09263 -6.610334,-3.77729 -0.658039,-0.24215 -1.292684,-0.42971 -1.89931,-0.57217 -4.36639,-1.02538 -7.442495,0.25932 -8.602678,0.58816 z" />
          <g
             id="g27478"
             transform="translate(-4.8406107,-166.8366)"
             style="fill:#00be5a;fill-opacity:1">
            <g
               id="g2188-2-9-4-3"
               style="fill:#00be5a;fill-opacity:1"
               transform="matrix(6.9641189e-8,14.644994,-19.336741,9.1951804e-8,300.32226,141.46704)">
              <path
                 style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
                 d="m 16.13938,5.3266 h 0.26458 c 0.12354,0 0.247406,0.06176 0.344383,0.1264118 0.05591,0.046865 0.141622,0.084894 0.173603,0.1006421 0.0127,0.003 0.01117,0.00253 0.01117,0.00253 0,0 8.03e-4,0.00145 -0.003,-0.011157 -0.01774,-0.0315 -0.07352,-0.1135646 -0.142944,-0.1502587 C 16.685714,5.3271298 16.545,5.2566 16.40396,5.2566 h -0.26458 z"
                 id="path1101-6-9-2-83" />
              <path
                 style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
                 d="m 16.13938,5.2816 h 0.26458 c 0.14104,0 0.281754,-0.07053 0.383212,-0.1381682 0.06942,-0.036694 0.125201,-0.118759 0.142971,-0.1502545 0.0038,-0.012612 0.003,-0.011157 0.003,-0.011157 0,0 0.0015,-4.753e-4 -0.01117,0.00253 -0.03198,0.015748 -0.117689,0.053778 -0.173603,0.1006421 C 16.651366,5.1498398 16.5275,5.2116 16.40396,5.2116 h -0.26458 z"
                 id="path1101-4-7-6-2-33" />
            </g>
            <path
               id="rect3698-3-3-9-6-5-0-2-8"
               style="vector-effect:none;fill:#00be5a;fill-opacity:1;fill-rule:evenodd;stroke-width:3.91082;stroke-linejoin:bevel;stop-color:#000000"
               d="m 207.28366,355.53032 -3.38322,20.9263 c -0.309,1.91125 -1.95584,3.48772 -4.38527,3.48772 h -2.00402 c -2.42941,0 -4.07625,-1.57647 -4.38525,-3.48772 l -3.38322,-20.9263 c -0.309,-1.91125 1.95582,-3.48772 4.38525,-3.48772 h 8.77048 c 2.42943,0 4.69425,1.57646 4.38525,3.48772 z" />
            <circle
               style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
               id="path22550-4-0-0"
               cx="204.2769"
               cy="366.4433"
               r="2.0954933" />
            <circle
               style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
               id="path22550-4-2-5-4"
               cx="192.59309"
               cy="366.4433"
               r="2.0954933" />
          </g>
        </g>
        <g
           id="g31972-9"
           transform="rotate(120,132.28991,132.29016)"
           style="fill:#00be5a;fill-opacity:1">
          <path
             style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.55391;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path15079-9-6-2-0"
             transform="rotate(150)"
             d="m -48.790388,-278.76829 c 1.375733,0.38008 3.778011,1.69906 6.747005,3.78375 0.413023,0.29 0.847811,0.60231 1.300374,0.93557 1.26955,0.93489 2.876917,2.21115 4.689357,4.04813 1.745816,1.76945 3.714063,4.08962 5.797099,7.198 1.753533,2.61668 3.582194,5.78446 5.400025,9.60883 3.149699,6.62638 6.227597,15.15117 8.70143,25.7326 2.21665,9.48138 3.907031,20.45533 4.684311,32.68964 1.4914398,23.47515 -0.715185,46.65785 -5.805647,65.7865 -2.588599,9.7273 -5.814892,17.99733 -9.417093,24.59797 -1.890488,3.46411 -3.853175,6.411905 -5.843267,8.866476 -2.104906,2.59618 -4.204339,4.596491 -6.245487,6.08719 -2.051741,1.498435 -4.060366,2.504859 -6.005794,3.069223 -1.840769,0.534001 -3.662431,0.683925 -5.481457,0.45655 -1.764571,-0.220567 -3.612136,-0.807338 -5.533666,-1.846961 -1.887895,-1.021425 -3.846757,-2.480677 -5.834665,-4.444432 -3.952422,-3.904402 -7.83097,-9.618406 -11.31471,-17.156686 -3.524148,-7.62572 -6.50198,-16.80959 -8.748861,-27.26278 -5.091735,-23.68835 -5.202376,-47.76946 -3.204601,-66.67525 1.364944,-12.91704 3.647167,-23.1336 5.78832,-30.55392 0.583592,-2.02248 1.156821,-3.8388 1.698441,-5.4505 3.450532,-10.26773 6.988836,-16.35101 8.387316,-18.66612 l -8.466458,-5.11429 c -1.655846,2.74117 -5.553619,9.4909 -9.296835,20.62956 -0.584111,1.73813 -1.201238,3.68757 -1.828899,5.84963 -2.304715,7.93888 -4.738337,18.71499 -6.211046,32.24007 -2.147162,19.71914 -2.129523,44.90359 3.184464,69.86869 2.349584,11.03835 5.524422,20.97173 9.420182,29.41818 3.831707,8.307568 8.350964,15.153101 13.430329,20.144979 2.576889,2.532504 5.312866,4.602366 8.188816,6.136377 2.947667,1.572265 6.016664,2.567182 9.155763,2.925666 3.252611,0.371448 6.458413,0.04557 9.547901,-0.90012 3.229096,-0.988431 6.247863,-2.627023 9.037488,-4.737875 2.777282,-2.101511 5.433965,-4.740086 7.95145,-7.905101 2.380537,-2.992843 4.634422,-6.453877 6.740464,-10.363173 4.002689,-7.429913 7.455191,-16.452953 10.1671393,-26.762323 5.3252774,-20.24384 7.54173605,-44.48566 5.8242559,-68.86016 -0.8971905,-12.73294 -2.7776961,-24.20959 -5.2759074,-34.17451 -2.7874038,-11.11846 -6.2980878,-20.1928 -10.0406478,-27.31206 -2.161419,-4.11154 -4.38578,-7.54655 -6.591639,-10.38984 -2.624708,-3.38317 -5.195626,-5.89373 -7.582928,-7.73618 -2.49073,-1.92226 -4.749815,-3.09263 -6.610334,-3.77729 -0.658039,-0.24215 -1.292684,-0.42971 -1.89931,-0.57217 -4.36639,-1.02538 -7.442495,0.25932 -8.602678,0.58816 z" />
          <g
             id="g27478-6"
             transform="translate(-4.8406107,-166.8366)"
             style="fill:#00be5a;fill-opacity:1">
            <g
               id="g2188-2-9-4-3-8"
               style="fill:#00be5a;fill-opacity:1"
               transform="matrix(6.9641189e-8,14.644994,-19.336741,9.1951804e-8,300.32226,141.46704)">
              <path
                 style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
                 d="m 16.13938,5.3266 h 0.26458 c 0.12354,0 0.247406,0.06176 0.344383,0.1264118 0.05591,0.046865 0.141622,0.084894 0.173603,0.1006421 0.0127,0.003 0.01117,0.00253 0.01117,0.00253 0,0 8.03e-4,0.00145 -0.003,-0.011157 -0.01774,-0.0315 -0.07352,-0.1135646 -0.142944,-0.1502587 C 16.685714,5.3271298 16.545,5.2566 16.40396,5.2566 h -0.26458 z"
                 id="path1101-6-9-2-83-7" />
              <path
                 style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
                 d="m 16.13938,5.2816 h 0.26458 c 0.14104,0 0.281754,-0.07053 0.383212,-0.1381682 0.06942,-0.036694 0.125201,-0.118759 0.142971,-0.1502545 0.0038,-0.012612 0.003,-0.011157 0.003,-0.011157 0,0 0.0015,-4.753e-4 -0.01117,0.00253 -0.03198,0.015748 -0.117689,0.053778 -0.173603,0.1006421 C 16.651366,5.1498398 16.5275,5.2116 16.40396,5.2116 h -0.26458 z"
                 id="path1101-4-7-6-2-33-9" />
            </g>
            <path
               id="rect3698-3-3-9-6-5-0-2-8-0"
               style="vector-effect:none;fill:#00be5a;fill-opacity:1;fill-rule:evenodd;stroke-width:3.91082;stroke-linejoin:bevel;stop-color:#000000"
               d="m 207.28366,355.53032 -3.38322,20.9263 c -0.309,1.91125 -1.95584,3.48772 -4.38527,3.48772 h -2.00402 c -2.42941,0 -4.07625,-1.57647 -4.38525,-3.48772 l -3.38322,-20.9263 c -0.309,-1.91125 1.95582,-3.48772 4.38525,-3.48772 h 8.77048 c 2.42943,0 4.69425,1.57646 4.38525,3.48772 z" />
            <circle
               style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
               id="path22550-4-0-0-3"
               cx="204.2769"
               cy="366.4433"
               r="2.0954933" />
            <circle
               style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
               id="path22550-4-2-5-4-3"
               cx="192.59309"
               cy="366.4433"
               r="2.0954933" />
          </g>
        </g>
        <g
           id="g31972-2"
           transform="rotate(-120,132.29009,132.29016)"
           style="fill:#00be5a;fill-opacity:1">
          <path
             style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.55391;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
             id="path15079-9-6-2-6"
             transform="rotate(150)"
             d="m -48.790388,-278.76829 c 1.375733,0.38008 3.778011,1.69906 6.747005,3.78375 0.413023,0.29 0.847811,0.60231 1.300374,0.93557 1.26955,0.93489 2.876917,2.21115 4.689357,4.04813 1.745816,1.76945 3.714063,4.08962 5.797099,7.198 1.753533,2.61668 3.582194,5.78446 5.400025,9.60883 3.149699,6.62638 6.227597,15.15117 8.70143,25.7326 2.21665,9.48138 3.907031,20.45533 4.684311,32.68964 1.4914398,23.47515 -0.715185,46.65785 -5.805647,65.7865 -2.588599,9.7273 -5.814892,17.99733 -9.417093,24.59797 -1.890488,3.46411 -3.853175,6.411905 -5.843267,8.866476 -2.104906,2.59618 -4.204339,4.596491 -6.245487,6.08719 -2.051741,1.498435 -4.060366,2.504859 -6.005794,3.069223 -1.840769,0.534001 -3.662431,0.683925 -5.481457,0.45655 -1.764571,-0.220567 -3.612136,-0.807338 -5.533666,-1.846961 -1.887895,-1.021425 -3.846757,-2.480677 -5.834665,-4.444432 -3.952422,-3.904402 -7.83097,-9.618406 -11.31471,-17.156686 -3.524148,-7.62572 -6.50198,-16.80959 -8.748861,-27.26278 -5.091735,-23.68835 -5.202376,-47.76946 -3.204601,-66.67525 1.364944,-12.91704 3.647167,-23.1336 5.78832,-30.55392 0.583592,-2.02248 1.156821,-3.8388 1.698441,-5.4505 3.450532,-10.26773 6.988836,-16.35101 8.387316,-18.66612 l -8.466458,-5.11429 c -1.655846,2.74117 -5.553619,9.4909 -9.296835,20.62956 -0.584111,1.73813 -1.201238,3.68757 -1.828899,5.84963 -2.304715,7.93888 -4.738337,18.71499 -6.211046,32.24007 -2.147162,19.71914 -2.129523,44.90359 3.184464,69.86869 2.349584,11.03835 5.524422,20.97173 9.420182,29.41818 3.831707,8.307568 8.350964,15.153101 13.430329,20.144979 2.576889,2.532504 5.312866,4.602366 8.188816,6.136377 2.947667,1.572265 6.016664,2.567182 9.155763,2.925666 3.252611,0.371448 6.458413,0.04557 9.547901,-0.90012 3.229096,-0.988431 6.247863,-2.627023 9.037488,-4.737875 2.777282,-2.101511 5.433965,-4.740086 7.95145,-7.905101 2.380537,-2.992843 4.634422,-6.453877 6.740464,-10.363173 4.002689,-7.429913 7.455191,-16.452953 10.1671393,-26.762323 5.3252774,-20.24384 7.54173605,-44.48566 5.8242559,-68.86016 -0.8971905,-12.73294 -2.7776961,-24.20959 -5.2759074,-34.17451 -2.7874038,-11.11846 -6.2980878,-20.1928 -10.0406478,-27.31206 -2.161419,-4.11154 -4.38578,-7.54655 -6.591639,-10.38984 -2.624708,-3.38317 -5.195626,-5.89373 -7.582928,-7.73618 -2.49073,-1.92226 -4.749815,-3.09263 -6.610334,-3.77729 -0.658039,-0.24215 -1.292684,-0.42971 -1.89931,-0.57217 -4.36639,-1.02538 -7.442495,0.25932 -8.602678,0.58816 z" />
          <g
             id="g27478-5"
             transform="translate(-4.8406107,-166.8366)"
             style="fill:#00be5a;fill-opacity:1">
            <g
               id="g2188-2-9-4-3-2"
               style="fill:#00be5a;fill-opacity:1"
               transform="matrix(6.9641189e-8,14.644994,-19.336741,9.1951804e-8,300.32226,141.46704)">
              <path
                 style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
                 d="m 16.13938,5.3266 h 0.26458 c 0.12354,0 0.247406,0.06176 0.344383,0.1264118 0.05591,0.046865 0.141622,0.084894 0.173603,0.1006421 0.0127,0.003 0.01117,0.00253 0.01117,0.00253 0,0 8.03e-4,0.00145 -0.003,-0.011157 -0.01774,-0.0315 -0.07352,-0.1135646 -0.142944,-0.1502587 C 16.685714,5.3271298 16.545,5.2566 16.40396,5.2566 h -0.26458 z"
                 id="path1101-6-9-2-83-6" />
              <path
                 style="fill:#00be5a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.185206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
                 d="m 16.13938,5.2816 h 0.26458 c 0.14104,0 0.281754,-0.07053 0.383212,-0.1381682 0.06942,-0.036694 0.125201,-0.118759 0.142971,-0.1502545 0.0038,-0.012612 0.003,-0.011157 0.003,-0.011157 0,0 0.0015,-4.753e-4 -0.01117,0.00253 -0.03198,0.015748 -0.117689,0.053778 -0.173603,0.1006421 C 16.651366,5.1498398 16.5275,5.2116 16.40396,5.2116 h -0.26458 z"
                 id="path1101-4-7-6-2-33-5" />
            </g>
            <path
               id="rect3698-3-3-9-6-5-0-2-8-8"
               style="vector-effect:none;fill:#00be5a;fill-opacity:1;fill-rule:evenodd;stroke-width:3.91082;stroke-linejoin:bevel;stop-color:#000000"
               d="m 207.28366,355.53032 -3.38322,20.9263 c -0.309,1.91125 -1.95584,3.48772 -4.38527,3.48772 h -2.00402 c -2.42941,0 -4.07625,-1.57647 -4.38525,-3.48772 l -3.38322,-20.9263 c -0.309,-1.91125 1.95582,-3.48772 4.38525,-3.48772 h 8.77048 c 2.42943,0 4.69425,1.57646 4.38525,3.48772 z" />
            <circle
               style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
               id="path22550-4-0-0-7"
               cx="204.2769"
               cy="366.4433"
               r="2.0954933" />
            <circle
               style="fill:#00be5a;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
               id="path22550-4-2-5-4-9"
               cx="192.59309"
               cy="366.4433"
               r="2.0954933" />
          </g>
        </g>
      </g>
    </g>
    <text
       xml:space="preserve"
       style="font-weight:bold;font-size:46.2064px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';letter-spacing:0px;word-spacing:0px;fill:#00be5a;fill-opacity:1;stroke-width:0.115516"
       x="113.23109"
       y="97.636375"
       id="text5299"><tspan
         id="tspan5297"
         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:46.2064px;font-family:sans-serif;-inkscape-font-specification:sans-serif;fill:#00be5a;fill-opacity:1;stroke-width:0.115516"
         x="113.23109"
         y="97.636375">ReactPy</tspan></text>
  </g>
</svg>

branding/png/reactpy-logo-padded.png METASEP

branding/png/reactpy-logo-landscape.png METASEP

branding/png/reactpy-logo-landscape-padded.png METASEP

branding/ico/reactpy-logo.ico METASEP

docs/source/conf.py METASEP
# -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/master/config

import sys
from doctest import DONT_ACCEPT_TRUE_FOR_1, ELLIPSIS, NORMALIZE_WHITESPACE
from pathlib import Path


# -- Path Setup --------------------------------------------------------------

THIS_DIR = Path(__file__).parent
ROOT_DIR = THIS_DIR.parent.parent

# project path
sys.path.insert(0, str(ROOT_DIR))

# extension path
sys.path.insert(0, str(THIS_DIR / "_exts"))


# -- Project information -----------------------------------------------------

project = "ReactPy"
title = "ReactPy"
description = (
    "ReactPy is a Python web framework for building interactive websites without needing "
    "a single line of Javascript. It can be run standalone, in a Jupyter Notebook, or "
    "as part of an existing application."
)
copyright = "2023, Ryan Morshead"
author = "Ryan Morshead"

# -- Common External Links ---------------------------------------------------

extlinks = {
    "issue": (
        "https://github.com/reactive-python/reactpy/issues/%s",
        "#",
    ),
    "pull": (
        "https://github.com/reactive-python/reactpy/pull/%s",
        "#",
    ),
    "discussion": (
        "https://github.com/reactive-python/reactpy/discussions/%s",
        "#",
    ),
    "discussion-type": (
        "https://github.com/reactive-python/reactpy/discussions/categories/%s",
        "",
    ),
    "commit": (
        "https://github.com/reactive-python/reactpy/commit/%s",
        "",
    ),
}
extlinks_detect_hardcoded_links = True


# -- General configuration ---------------------------------------------------

# If your documentatirston needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
    "sphinx.ext.autodoc",
    "sphinx.ext.intersphinx",
    "sphinx.ext.coverage",
    "sphinx.ext.viewcode",
    "sphinx.ext.napoleon",
    "sphinx.ext.extlinks",
    # third party extensions
    "sphinx_copybutton",
    "sphinx_reredirects",
    "sphinx_design",
    "sphinxext.opengraph",
    # custom extensions
    "async_doctest",
    "autogen_api_docs",
    "copy_vdom_json_schema",
    "reactpy_view",
    "patched_html_translator",
    "reactpy_example",
    "build_custom_js",
    "custom_autosectionlabel",
]

# Add any paths that contain templates here, relative to this directory.
# templates_path = ["templates"]

# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = ".rst"

# The master toctree document.
master_doc = "index"

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = "en"

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = [
    "_custom_js",
]

# The name of the Pygments (syntax highlighting) style to use.
pygments_style = None

# The default language to highlight source code in.
highlight_language = "python3"

# Controls how sphinx.ext.autodoc represents typehints in the function signature
autodoc_typehints = "description"

# -- Doc Test Configuration -------------------------------------------------------

doctest_default_flags = NORMALIZE_WHITESPACE | ELLIPSIS | DONT_ACCEPT_TRUE_FOR_1

# -- Extension Configuration ------------------------------------------------------


# -- sphinx.ext.autosectionlabel ---

autosectionlabel_skip_docs = ["_auto/apis"]


# -- sphinx.ext.autodoc --

# show base classes for autodoc
autodoc_default_options = {
    "show-inheritance": True,
    "member-order": "bysource",
}
# order autodoc members by their order in the source
autodoc_member_order = "bysource"


# -- sphinx_reredirects --

redirects = {
    "package-api": "_autogen/user-apis.html",
    "configuration-options": "_autogen/dev-apis.html#configuration-options",
    "examples": "creating-interfaces/index.html",
}


# -- sphinxext.opengraph --

ogp_site_url = "https://reactpy.dev/"
ogp_image = "https://raw.githubusercontent.com/reactive-python/reactpy/main/branding/png/reactpy-logo-landscape-padded.png"
# We manually specify this below
# ogp_description_length = 200
ogp_type = "website"
ogp_custom_meta_tags = [
    # Open Graph Meta Tags
    f'<meta property="og:title" content="{title}">',
    f'<meta property="og:description" content="{description}">',
    # Twitter Meta Tags
    '<meta name="twitter:card" content="summary_large_image">',
    '<meta name="twitter:creator" content="@rmorshea">',
    '<meta name="twitter:site" content="@rmorshea">',
]


# -- Options for HTML output -------------------------------------------------

# Set the page title
html_title = title

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
html_theme = "furo"
html_logo = str(ROOT_DIR / "branding" / "svg" / "reactpy-logo-landscape.svg")
html_favicon = str(ROOT_DIR / "branding" / "ico" / "reactpy-logo.ico")

# Theme options are theme-specific and customize the look and feel of a theme
# further.  For a list of options available for each theme, see the
# documentation.

html_theme_options = {
    "light_css_variables": {
        # furo
        "admonition-title-font-size": "1rem",
        "admonition-font-size": "1rem",
        # sphinx-design
        "sd-color-info": "var(--color-admonition-title-background--note)",
        "sd-color-warning": "var(--color-admonition-title-background--warning)",
        "sd-color-danger": "var(--color-admonition-title-background--danger)",
        "sd-color-info-text": "var(--color-admonition-title--note)",
        "sd-color-warning-text": "var(--color-admonition-title--warning)",
        "sd-color-danger-text": "var(--color-admonition-title--danger)",
    },
}

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]

# These paths are either relative to html_static_path
# or fully qualified paths (eg. https://...)
css_dir = THIS_DIR / "_static" / "css"
html_css_files = [
    str(p.relative_to(THIS_DIR / "_static")) for p in css_dir.glob("*.css")
]

# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself.  Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}


# -- Options for Sphinx Panels -----------------------------------------------

panels_css_variables = {
    "tabs-color-label-active": "rgb(106, 176, 221)",
    "tabs-color-label-inactive": "rgb(201, 225, 250)",
    "tabs-color-overline": "rgb(201, 225, 250)",
    "tabs-color-underline": "rgb(201, 225, 250)",
}

# -- Options for HTMLHelp output ---------------------------------------------

# Output file base name for HTML help builder.
htmlhelp_basename = "ReactPydoc"


# -- Options for LaTeX output ------------------------------------------------

# latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
# }

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
#  author, documentclass [howto, manual, or own class]).
latex_documents = [(master_doc, "ReactPy.tex", html_title, "Ryan Morshead", "manual")]


# -- Options for manual page output ------------------------------------------

# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [(master_doc, "reactpy", html_title, [author], 1)]


# -- Options for Texinfo output ----------------------------------------------

# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
#  dir menu entry, description, category)
texinfo_documents = [
    (
        master_doc,
        "ReactPy",
        html_title,
        author,
        "ReactPy",
        "One line description of project.",
        "Miscellaneous",
    )
]

# -- Options for Sphinx-Autodoc-Typehints output -------------------------------------------------

set_type_checking_flag = False

# -- Options for Epub output -------------------------------------------------

# Bibliographic Dublin Core info.
epub_title = project

# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#
# epub_identifier = ''

# A unique identification for the text.
#
# epub_uid = ''

# A list of files that should not be packed into the epub file.
epub_exclude_files = ["search.html"]

# -- Options for intersphinx extension ---------------------------------------

# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
    "https://docs.python.org/": None,
    "pyalect": ("https://pyalect.readthedocs.io/en/latest", None),
    "sanic": ("https://sanic.readthedocs.io/en/latest/", None),
    "tornado": ("https://www.tornadoweb.org/en/stable/", None),
    "flask": ("https://flask.palletsprojects.com/en/1.1.x/", None),
}

# -- Options for todo extension ----------------------------------------------

# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True

docs/README.md METASEP
# ReactPy's Documentation

We provide two main ways to run the docs. Both use
[`nox`](https://pypi.org/project/nox/):

- `nox -s docs` - displayes the docs and rebuilds when files are modified.
- `nox -s docs-in-docker` - builds a docker image and runs the docs from there.

If any changes to the core of the documentation are made (i.e. to non-`*.rst` files),
then you should run a manual test of the documentation using the `docs_in_docker`
session.

If you wish to build and run the docs by hand you need to perform two commands, each
being run from the root of the repository:

- `sphinx-build -b html docs/source docs/build`
- `python scripts/run_docs.py`

The first command constructs the static HTML and any Javascript. The latter actually
runs the web server that serves the content.

docs/Dockerfile METASEP
FROM python:3.9

WORKDIR /app/

# Install NodeJS
# --------------
RUN curl -sL https://deb.nodesource.com/setup_14.x  | bash -
RUN apt-get install -yq nodejs build-essential
RUN npm install -g npm@8.5.0

# Create Python Venv
# ------------------
ENV VIRTUAL_ENV=/opt/venv
RUN python3 -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
RUN pip install --upgrade pip

# Install ReactPy
# ------------
COPY requirements ./requirements
RUN pip install -r requirements/build-docs.txt

COPY src ./src
COPY scripts ./scripts
COPY setup.py ./
COPY pyproject.toml ./
COPY MANIFEST.in ./
COPY README.md ./
RUN pip install .[all]

# COPY License
# -----------
COPY LICENSE /app/

# Build the Docs
# --------------
COPY docs/__init__.py ./docs/
COPY docs/app.py ./docs/
COPY docs/examples.py ./docs/
COPY docs/source ./docs/source
COPY branding ./branding
RUN sphinx-build -v -W -b html docs/source docs/build

# Define Entrypoint
# -----------------
ENV PORT 5000
ENV REACTPY_DEBUG_MODE=1
ENV REACTPY_CHECK_VDOM_SPEC=0
CMD python scripts/run_docs.py

docs/.gitignore METASEP
build
source/_auto
source/_static/custom.js
source/vdom-json-schema.json

requirements.txt METASEP
-r requirements/build-docs.txt
-r requirements/build-pkg.txt
-r requirements/check-style.txt
-r requirements/check-types.txt
-r requirements/make-release.txt
-r requirements/pkg-deps.txt
-r requirements/pkg-extras.txt
-r requirements/test-env.txt
-r requirements/nox-deps.txt

pyproject.toml METASEP
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"

[tool.isort]
multi_line_output = 3
force_grid_wrap = 0
use_parentheses = "True"
ensure_newline_before_comments = "True"
include_trailing_comma = "True"
line_length = 88
lines_after_imports = 2

[tool.mypy]
incremental = false
ignore_missing_imports = true
warn_unused_configs = true
warn_redundant_casts = true
warn_unused_ignores = true

[tool.pytest.ini_options]
testpaths = "tests"
xfail_strict = true
markers = ["slow: marks tests as slow (deselect with '-m \"not slow\"')"]
python_files = "*asserts.py test_*.py"
asyncio_mode = "auto"

[tool.coverage.report]
fail_under = 100
show_missing = true
skip_covered = true
sort = "Name"
exclude_lines = [
    # These are regex patterns
    'pragma: no cover',
    '\.\.\.',
    'raise NotImplementedError',
    'if TYPE_CHECKING[\s:]',
]
omit = [
    "src/reactpy/__main__.py",
]

[tool.pydocstyle]
inherit = false
match = '.*\.py'
convention = "google"
add_ignore = ["D100", "D101", "D102", "D103", "D104", "D105", "D107", "D412", "D415"]

[tool.flake8]
ignore = ["E203", "E266", "E501", "W503", "F811", "N802", "N806"]
per-file-ignores = [
    # sometimes this is required in order to hide setup for an example
    "docs/*/_examples/*.py:E402",
]
max-line-length = 88
max-complexity = 20
select = ["B", "C", "E", "F", "W", "T4", "B9", "N", "RPY"]
exclude = ["**/node_modules/*", ".eggs/*", ".tox/*"]
# -- flake8-tidy-imports --
ban-relative-imports = "true"

README.md METASEP
# <img src="https://raw.githubusercontent.com/reactive-python/reactpy/main/branding/svg/reactpy-logo-square.svg" align="left" height="45"/> ReactPy

<p>
    <a href="https://github.com/reactive-python/reactpy/actions">
        <img src="https://github.com/reactive-python/reactpy/workflows/test/badge.svg?event=push">
    </a>
    <a href="https://pypi.org/project/reactpy/">
        <img src="https://img.shields.io/pypi/v/reactpy.svg?label=PyPI">
    </a>
    <a href="https://github.com/reactive-python/reactpy/blob/main/LICENSE">
        <img src="https://img.shields.io/badge/License-MIT-purple.svg">
    </a>
    <a href="https://reactpy.dev/">
        <img src="https://img.shields.io/website?down_message=offline&label=Docs&logo=read-the-docs&logoColor=white&up_message=online&url=https%3A%2F%2Freactpy.dev%2Fdocs%2Findex.html">
    </a>
    <a href="https://discord.gg/uNb5P4hA9X">
        <img src="https://img.shields.io/discord/1111078259854168116?label=Discord&logo=discord">
    </a>
</p>

---

[ReactPy](https://reactpy.dev/) is a library for building user interfaces in Python without Javascript. ReactPy interfaces are made from components that look and behave similar to those found in [ReactJS](https://reactjs.org/). Designed with simplicity in mind, ReactPy can be used by those without web development experience while also being powerful enough to grow with your ambitions.

<table align="center">
    <thead>
        <tr>
            <th colspan="2" style="text-align: center">Supported Backends</th>
        <tr>
            <th style="text-align: center">Built-in</th>
            <th style="text-align: center">External</th>
        </tr>
    </thead>
    <tbody>
        <tr>
        <td>
            <a href="https://reactpy.dev/docs/guides/getting-started/installing-reactpy.html#officially-supported-servers">
                Flask, FastAPI, Sanic, Tornado
            </a>
        </td>
        <td>
            <a href="https://github.com/reactive-python/reactpy-django">Django</a>,
            <a href="https://github.com/reactive-python/reactpy-jupyter">Jupyter</a>,
            <a href="https://github.com/idom-team/idom-dash">Plotly-Dash</a>
        </td>
        </tr>
    </tbody>
</table>

# At a Glance

To get a rough idea of how to write apps in ReactPy, take a look at this tiny _Hello World_ application.

```python
from reactpy import component, html, run

@component
def hello_world():
    return html.h1("Hello, World!")

run(hello_world)
```

# Resources

Follow the links below to find out more about this project.

-   [Try ReactPy (Jupyter Notebook)](https://mybinder.org/v2/gh/reactive-python/reactpy-jupyter/main?urlpath=lab/tree/notebooks/introduction.ipynb)
-   [Documentation](https://reactpy.dev/)
-   [GitHub Discussions](https://github.com/reactive-python/reactpy/discussions)
-   [Discord](https://discord.gg/uNb5P4hA9X)
-   [Contributor Guide](https://reactpy.dev/docs/developing-reactpy/contributor-guide.html)
-   [Code of Conduct](https://github.com/reactive-python/reactpy/blob/main/CODE_OF_CONDUCT.md)

CODE_OF_CONDUCT.md METASEP
# Contributor Covenant Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment
include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or
 advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
 address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
 professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at ryan.morshead@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

[homepage]: https://www.contributor-covenant.org

For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

tasks.py METASEP

from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
    return PackageInfo(
        name=proj_metadata["name"],
        path=pkg_dir,
        language="py",
        version=proj_metadata["version"],
    )


def make_js_pkg_info(pkg_dir: Path) -> PackageInfo:
    with (pkg_dir / "package.json").open() as f:
        pkg_json = json.load(f)
    return PackageInfo(
        name=pkg_json["name"],
        path=pkg_dir,
        language="js",
        version=pkg_json["version"],
    )


@dataclass
class PackageInfo:
    name: str
    path: Path
    language: LanguageName
    version: str


def get_current_tags(context: Context) -> set[str]:
    """Get tags for the current commit"""
    # check if unstaged changes
    try:
        context.run("git diff --cached --exit-code", hide=True)
        context.run("git diff --exit-code", hide=True)
    except Exception:
        log.error("Cannot create a tag - there are uncommited changes")
        return set()

    tags_per_commit: dict[str, list[str]] = {}
    for commit, tag in map(
        str.split,
        context.run(
            r"git for-each-ref --format '%(objectname) %(refname:short)' refs/tags",
            hide=True,
        ).stdout.splitlines(),
    ):
        tags_per_commit.setdefault(commit, []).append(tag)

    current_commit = context.run(
        "git rev-parse HEAD", silent=True, external=True
    ).stdout.strip()
    tags = set(tags_per_commit.get(current_commit, set()))

    if not tags:
        log.error("No tags found for current commit")

    for t in tags:
        if not TAG_PATTERN.match(t):
            msg = f"Invalid tag: {t}"
            raise Exit(msg)

    log.info(f"Found tags: {tags}")

    return tags


def parse_tag(tag: str) -> TagInfo:
    match = TAG_PATTERN.match(tag)
    if not match:
        msg = f"Invalid tag: {tag}"
        raise Exit(msg)

    version = match.group("version")
    if not semver.Version.is_valid(version):
        raise Exit(f"Invalid version: {version} in tag {tag}")

    return TagInfo(tag=tag, name=match.group("name"), version=match.group("version"))


@dataclass
class TagInfo:
    tag: str
    name: str
    version: str


def prepare_js_release(
    context: Context, package: PackageInfo
) -> Callable[[bool], None]:
    node_auth_token = os.getenv("NODE_AUTH_TOKEN")
    if node_auth_token is None:
        msg = "NODE_AUTH_TOKEN environment variable must be set"
        raise Exit(msg)

    with context.cd(JS_DIR):
        context.run("npm ci")
        context.run("npm run build")

    def publish(dry_run: bool) -> None:
        with context.cd(JS_DIR):
            if dry_run:
                context.run(f"npm --workspace {package.name} pack --dry-run")
                return
            context.run(
                f"npm --workspace {package.name} publish --access public",
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
    return PackageInfo(
        name=proj_metadata["name"],
        path=pkg_dir,
        language="py",
        version=proj_metadata["version"],
    )


def make_js_pkg_info(pkg_dir: Path) -> PackageInfo:
    with (pkg_dir / "package.json").open() as f:
        pkg_json = json.load(f)
    return PackageInfo(
        name=pkg_json["name"],
        path=pkg_dir,
        language="js",
        version=pkg_json["version"],
    )


@dataclass
class PackageInfo:
    name: str
    path: Path
    language: LanguageName
    version: str


def get_current_tags(context: Context) -> set[str]:
    """Get tags for the current commit"""
    # check if unstaged changes
    try:
        context.run("git diff --cached --exit-code", hide=True)
        context.run("git diff --exit-code", hide=True)
    except Exception:
        log.error("Cannot create a tag - there are uncommited changes")
        return set()

    tags_per_commit: dict[str, list[str]] = {}
    for commit, tag in map(
        str.split,
        context.run(
            r"git for-each-ref --format '%(objectname) %(refname:short)' refs/tags",
            hide=True,
        ).stdout.splitlines(),
    ):
        tags_per_commit.setdefault(commit, []).append(tag)

    current_commit = context.run(
        "git rev-parse HEAD", silent=True, external=True
    ).stdout.strip()
    tags = set(tags_per_commit.get(current_commit, set()))

    if not tags:
        log.error("No tags found for current commit")

    for t in tags:
        if not TAG_PATTERN.match(t):
            msg = f"Invalid tag: {t}"
            raise Exit(msg)

    log.info(f"Found tags: {tags}")

    return tags


def parse_tag(tag: str) -> TagInfo:
    match = TAG_PATTERN.match(tag)
    if not match:
        msg = f"Invalid tag: {tag}"
        raise Exit(msg)

    version = match.group("version")
    if not semver.Version.is_valid(version):
        raise Exit(f"Invalid version: {version} in tag {tag}")

    return TagInfo(tag=tag, name=match.group("name"), version=match.group("version"))


@dataclass
class TagInfo:
    tag: str
    name: str
    version: str


def prepare_js_release(
    context: Context, package: PackageInfo
) -> Callable[[bool], None]:
    node_auth_token = os.getenv("NODE_AUTH_TOKEN")
    if node_auth_token is None:
        msg = "NODE_AUTH_TOKEN environment variable must be set"
        raise Exit(msg)

    with context.cd(JS_DIR):
        context.run("npm ci")
        context.run("npm run build")

    def publish(dry_run: bool) -> None:
        with context.cd(JS_DIR):
            if dry_run:
                context.run(f"npm --workspace {package.name} pack --dry-run")
                return
            context.run(
                f"npm --workspace {package.name} publish --access public",
                env={"NODE_AUTH_TOKEN": node_auth_token},
            )

infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
    return PackageInfo(
        name=proj_metadata["name"],
        path=pkg_dir,
        language="py",
        version=proj_metadata["version"],
    )


def make_js_pkg_info(pkg_dir: Path) -> PackageInfo:
    with (pkg_dir / "package.json").open() as f:
        pkg_json = json.load(f)
    return PackageInfo(
        name=pkg_json["name"],
        path=pkg_dir,
        language="js",
        version=pkg_json["version"],
    )


@dataclass
class PackageInfo:
    name: str
    path: Path
    language: LanguageName
    version: str


def get_current_tags(context: Context) -> set[str]:
    """Get tags for the current commit"""
    # check if unstaged changes
    try:
        context.run("git diff --cached --exit-code", hide=True)
        context.run("git diff --exit-code", hide=True)
    except Exception:
        log.error("Cannot create a tag - there are uncommited changes")
        return set()

    tags_per_commit: dict[str, list[str]] = {}
    for commit, tag in map(
        str.split,
        context.run(
            r"git for-each-ref --format '%(objectname) %(refname:short)' refs/tags",
            hide=True,
        ).stdout.splitlines(),
    ):
        tags_per_commit.setdefault(commit, []).append(tag)

    current_commit = context.run(
        "git rev-parse HEAD", silent=True, external=True
    ).stdout.strip()
    tags = set(tags_per_commit.get(current_commit, set()))

    if not tags:
        log.error("No tags found for current commit")

    for t in tags:
        if not TAG_PATTERN.match(t):
            msg = f"Invalid tag: {t}"
            raise Exit(msg)

    log.info(f"Found tags: {tags}")

    return tags


def parse_tag(tag: str) -> TagInfo:
    match = TAG_PATTERN.match(tag)
    if not match:
        msg = f"Invalid tag: {tag}"
        raise Exit(msg)

    version = match.group("version")
    if not semver.Version.is_valid(version):
        raise Exit(f"Invalid version: {version} in tag {tag}")

    return TagInfo(tag=tag, name=match.group("name"), version=match.group("version"))


@dataclass
class TagInfo:
    tag: str
    name: str
    version: str


def prepare_js_release(
    context: Context, package: PackageInfo
) -> Callable[[bool], None]:
    node_auth_token = os.getenv("NODE_AUTH_TOKEN")
    if node_auth_token is None:
        msg = "NODE_AUTH_TOKEN environment variable must be set"
        raise Exit(msg)

    with context.cd(JS_DIR):
        context.run("npm ci")
        context.run("npm run build")

    def publish(dry_run: bool) -> None:
        with context.cd(JS_DIR):
            if dry_run:
                context.run(f"npm --workspace {package.name} pack --dry-run")
                return
            context.run(
                f"npm --workspace {package.name} publish --access public",
                env={"NODE_AUTH_TOKEN": node_auth_token},
            )

    return publish


def prepare_py_release(
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
    return PackageInfo(
        name=proj_metadata["name"],
        path=pkg_dir,
        language="py",
        version=proj_metadata["version"],
    )


def make_js_pkg_info(pkg_dir: Path) -> PackageInfo:
    with (pkg_dir / "package.json").open() as f:
        pkg_json = json.load(f)
    return PackageInfo(
        name=pkg_json["name"],
        path=pkg_dir,
        language="js",
        version=pkg_json["version"],
    )


@dataclass
class PackageInfo:
    name: str
    path: Path
    language: LanguageName
    version: str


def get_current_tags(context: Context) -> set[str]:
    """Get tags for the current commit"""
    # check if unstaged changes
    try:
        context.run("git diff --cached --exit-code", hide=True)
        context.run("git diff --exit-code", hide=True)
    except Exception:
        log.error("Cannot create a tag - there are uncommited changes")
        return set()

    tags_per_commit: dict[str, list[str]] = {}
    for commit, tag in map(
        str.split,
        context.run(
            r"git for-each-ref --format '%(objectname) %(refname:short)' refs/tags",
            hide=True,
        ).stdout.splitlines(),
    ):
        tags_per_commit.setdefault(commit, []).append(tag)

    current_commit = context.run(
        "git rev-parse HEAD", silent=True, external=True
    ).stdout.strip()
    tags = set(tags_per_commit.get(current_commit, set()))

    if not tags:
        log.error("No tags found for current commit")

    for t in tags:
        if not TAG_PATTERN.match(t):
            msg = f"Invalid tag: {t}"
            raise Exit(msg)

    log.info(f"Found tags: {tags}")

    return tags


def parse_tag(tag: str) -> TagInfo:
    match = TAG_PATTERN.match(tag)
    if not match:
        msg = f"Invalid tag: {tag}"
        raise Exit(msg)

    version = match.group("version")
    if not semver.Version.is_valid(version):
        raise Exit(f"Invalid version: {version} in tag {tag}")

infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
infile
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
    return PackageInfo(
        name=proj_metadata["name"],
        path=pkg_dir,
        language="py",
        version=proj_metadata["version"],
    )


def make_js_pkg_info(pkg_dir: Path) -> PackageInfo:
    with (pkg_dir / "package.json").open() as f:
        pkg_json = json.load(f)
    return PackageInfo(
        name=pkg_json["name"],
        path=pkg_dir,
        language="js",
        version=pkg_json["version"],
    )


@dataclass
class PackageInfo:
    name: str
    path: Path
    language: LanguageName
    version: str


def get_current_tags(context: Context) -> set[str]:
    """Get tags for the current commit"""
    # check if unstaged changes
    try:
        context.run("git diff --cached --exit-code", hide=True)
        context.run("git diff --exit-code", hide=True)
    except Exception:
        log.error("Cannot create a tag - there are uncommited changes")
        return set()

    tags_per_commit: dict[str, list[str]] = {}
    for commit, tag in map(
        str.split,
        context.run(
            r"git for-each-ref --format '%(objectname) %(refname:short)' refs/tags",
            hide=True,
        ).stdout.splitlines(),
    ):
        tags_per_commit.setdefault(commit, []).append(tag)

    current_commit = context.run(
        "git rev-parse HEAD", silent=True, external=True
    ).stdout.strip()
    tags = set(tags_per_commit.get(current_commit, set()))

    if not tags:
        log.error("No tags found for current commit")

    for t in tags:
        if not TAG_PATTERN.match(t):
            msg = f"Invalid tag: {t}"
            raise Exit(msg)

    log.info(f"Found tags: {tags}")

    return tags


def parse_tag(tag: str) -> TagInfo:
    match = TAG_PATTERN.match(tag)
    if not match:
        msg = f"Invalid tag: {tag}"
        raise Exit(msg)

    version = match.group("version")
    if not semver.Version.is_valid(version):
        raise Exit(f"Invalid version: {version} in tag {tag}")

    return TagInfo(tag=tag, name=match.group("name"), version=match.group("version"))


@dataclass
class TagInfo:
    tag: str
    name: str
    version: str


def prepare_js_release(
    context: Context, package: PackageInfo
) -> Callable[[bool], None]:
    node_auth_token = os.getenv("NODE_AUTH_TOKEN")
    if node_auth_token is None:
        msg = "NODE_AUTH_TOKEN environment variable must be set"
        raise Exit(msg)

    with context.cd(JS_DIR):
        context.run("npm ci")
        context.run("npm run build")

    def publish(dry_run: bool) -> None:
        with context.cd(JS_DIR):
            if dry_run:
                context.run(f"npm --workspace {package.name} pack --dry-run")
                return
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
    return PackageInfo(
        name=proj_metadata["name"],
        path=pkg_dir,
        language="py",
        version=proj_metadata["version"],
    )


def make_js_pkg_info(pkg_dir: Path) -> PackageInfo:
    with (pkg_dir / "package.json").open() as f:
        pkg_json = json.load(f)
    return PackageInfo(
        name=pkg_json["name"],
        path=pkg_dir,
        language="js",
        version=pkg_json["version"],
    )


@dataclass
class PackageInfo:
    name: str
    path: Path
    language: LanguageName
    version: str


def get_current_tags(context: Context) -> set[str]:
    """Get tags for the current commit"""
    # check if unstaged changes
    try:
        context.run("git diff --cached --exit-code", hide=True)
        context.run("git diff --exit-code", hide=True)
    except Exception:
        log.error("Cannot create a tag - there are uncommited changes")
        return set()

    tags_per_commit: dict[str, list[str]] = {}
    for commit, tag in map(
        str.split,
        context.run(
            r"git for-each-ref --format '%(objectname) %(refname:short)' refs/tags",
            hide=True,
        ).stdout.splitlines(),
    ):
        tags_per_commit.setdefault(commit, []).append(tag)

    current_commit = context.run(
        "git rev-parse HEAD", silent=True, external=True
    ).stdout.strip()
    tags = set(tags_per_commit.get(current_commit, set()))

    if not tags:
        log.error("No tags found for current commit")

    for t in tags:
        if not TAG_PATTERN.match(t):
            msg = f"Invalid tag: {t}"
            raise Exit(msg)

    log.info(f"Found tags: {tags}")

    return tags


def parse_tag(tag: str) -> TagInfo:
    match = TAG_PATTERN.match(tag)
    if not match:
        msg = f"Invalid tag: {tag}"
        raise Exit(msg)

    version = match.group("version")
    if not semver.Version.is_valid(version):
        raise Exit(f"Invalid version: {version} in tag {tag}")

    return TagInfo(tag=tag, name=match.group("name"), version=match.group("version"))


@dataclass
class TagInfo:
    tag: str
    name: str
    version: str


def prepare_js_release(
    context: Context, package: PackageInfo
) -> Callable[[bool], None]:
    node_auth_token = os.getenv("NODE_AUTH_TOKEN")
    if node_auth_token is None:
        msg = "NODE_AUTH_TOKEN environment variable must be set"
        raise Exit(msg)

    with context.cd(JS_DIR):
        context.run("npm ci")
        context.run("npm run build")

    def publish(dry_run: bool) -> None:
        with context.cd(JS_DIR):
            if dry_run:
                context.run(f"npm --workspace {package.name} pack --dry-run")
                return
            context.run(
                f"npm --workspace {package.name} publish --access public",
                env={"NODE_AUTH_TOKEN": node_auth_token},
            )

    return publish


def prepare_py_release(
    context: Context, package: PackageInfo
) -> Callable[[bool], None]:
    twine_username = os.getenv("PYPI_USERNAME")
    twine_password = os.getenv("PYPI_PASSWORD")

    if not (twine_password and twine_username):
        msg = "PYPI_USERNAME and PYPI_PASSWORD environment variables must be set"
        raise Exit(msg)

    for build_dir_name in ["build", "dist"]:
        build_dir_path = Path.cwd() / build_dir_name
        if build_dir_path.exists():
            rmtree(str(build_dir_path))

    with context.cd(package.path):
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
    return PackageInfo(
        name=proj_metadata["name"],
        path=pkg_dir,
        language="py",
        version=proj_metadata["version"],
    )


def make_js_pkg_info(pkg_dir: Path) -> PackageInfo:
    with (pkg_dir / "package.json").open() as f:
        pkg_json = json.load(f)
    return PackageInfo(
        name=pkg_json["name"],
        path=pkg_dir,
        language="js",
        version=pkg_json["version"],
    )


@dataclass
class PackageInfo:
    name: str
    path: Path
    language: LanguageName
    version: str


def get_current_tags(context: Context) -> set[str]:
    """Get tags for the current commit"""
    # check if unstaged changes
    try:
        context.run("git diff --cached --exit-code", hide=True)
        context.run("git diff --exit-code", hide=True)
    except Exception:
        log.error("Cannot create a tag - there are uncommited changes")
        return set()

    tags_per_commit: dict[str, list[str]] = {}
    for commit, tag in map(
        str.split,
        context.run(
            r"git for-each-ref --format '%(objectname) %(refname:short)' refs/tags",
            hide=True,
        ).stdout.splitlines(),
    ):
        tags_per_commit.setdefault(commit, []).append(tag)

    current_commit = context.run(
        "git rev-parse HEAD", silent=True, external=True
    ).stdout.strip()
    tags = set(tags_per_commit.get(current_commit, set()))

    if not tags:
        log.error("No tags found for current commit")

    for t in tags:
        if not TAG_PATTERN.match(t):
            msg = f"Invalid tag: {t}"
            raise Exit(msg)

    log.info(f"Found tags: {tags}")

    return tags


def parse_tag(tag: str) -> TagInfo:
    match = TAG_PATTERN.match(tag)
    if not match:
        msg = f"Invalid tag: {tag}"
        raise Exit(msg)

    version = match.group("version")
    if not semver.Version.is_valid(version):
        raise Exit(f"Invalid version: {version} in tag {tag}")

    return TagInfo(tag=tag, name=match.group("name"), version=match.group("version"))


@dataclass
class TagInfo:
    tag: str
    name: str
    version: str


def prepare_js_release(
    context: Context, package: PackageInfo
) -> Callable[[bool], None]:
    node_auth_token = os.getenv("NODE_AUTH_TOKEN")
    if node_auth_token is None:
        msg = "NODE_AUTH_TOKEN environment variable must be set"
        raise Exit(msg)

    with context.cd(JS_DIR):
        context.run("npm ci")
        context.run("npm run build")

    def publish(dry_run: bool) -> None:
        with context.cd(JS_DIR):
            if dry_run:
                context.run(f"npm --workspace {package.name} pack --dry-run")
                return
            context.run(
                f"npm --workspace {package.name} publish --access public",
                env={"NODE_AUTH_TOKEN": node_auth_token},
            )

    return publish


def prepare_py_release(
    context: Context, package: PackageInfo
) -> Callable[[bool], None]:
    twine_username = os.getenv("PYPI_USERNAME")
    twine_password = os.getenv("PYPI_PASSWORD")

    if not (twine_password and twine_username):
        msg = "PYPI_USERNAME and PYPI_PASSWORD environment variables must be set"
        raise Exit(msg)

    for build_dir_name in ["build", "dist"]:
        build_dir_path = Path.cwd() / build_dir_name
        if build_dir_path.exists():
            rmtree(str(build_dir_path))

    with context.cd(package.path):
        context.run("hatch build")

    def publish(dry_run: bool):
        with context.cd(package.path):
            if dry_run:
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
    return PackageInfo(
        name=proj_metadata["name"],
        path=pkg_dir,
        language="py",
        version=proj_metadata["version"],
    )


def make_js_pkg_info(pkg_dir: Path) -> PackageInfo:
    with (pkg_dir / "package.json").open() as f:
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
    return PackageInfo(
        name=proj_metadata["name"],
        path=pkg_dir,
        language="py",
        version=proj_metadata["version"],
    )


def make_js_pkg_info(pkg_dir: Path) -> PackageInfo:
    with (pkg_dir / "package.json").open() as f:
        pkg_json = json.load(f)
    return PackageInfo(
        name=pkg_json["name"],
        path=pkg_dir,
        language="js",
        version=pkg_json["version"],
    )


@dataclass
class PackageInfo:
    name: str
    path: Path
    language: LanguageName
    version: str


def get_current_tags(context: Context) -> set[str]:
    """Get tags for the current commit"""
    # check if unstaged changes
    try:
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
    return PackageInfo(
        name=proj_metadata["name"],
        path=pkg_dir,
        language="py",
        version=proj_metadata["version"],
    )


def make_js_pkg_info(pkg_dir: Path) -> PackageInfo:
    with (pkg_dir / "package.json").open() as f:
        pkg_json = json.load(f)
    return PackageInfo(
        name=pkg_json["name"],
        path=pkg_dir,
        language="js",
        version=pkg_json["version"],
    )


@dataclass
class PackageInfo:
    name: str
    path: Path
    language: LanguageName
    version: str


def get_current_tags(context: Context) -> set[str]:
    """Get tags for the current commit"""
    # check if unstaged changes
    try:
        context.run("git diff --cached --exit-code", hide=True)
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
    return PackageInfo(
        name=proj_metadata["name"],
        path=pkg_dir,
        language="py",
        version=proj_metadata["version"],
    )


def make_js_pkg_info(pkg_dir: Path) -> PackageInfo:
    with (pkg_dir / "package.json").open() as f:
        pkg_json = json.load(f)
    return PackageInfo(
        name=pkg_json["name"],
        path=pkg_dir,
        language="js",
        version=pkg_json["version"],
    )


@dataclass
class PackageInfo:
    name: str
    path: Path
    language: LanguageName
    version: str


def get_current_tags(context: Context) -> set[str]:
    """Get tags for the current commit"""
    # check if unstaged changes
    try:
        context.run("git diff --cached --exit-code", hide=True)
        context.run("git diff --exit-code", hide=True)
    except Exception:
        log.error("Cannot create a tag - there are uncommited changes")
        return set()

    tags_per_commit: dict[str, list[str]] = {}
    for commit, tag in map(
        str.split,
        context.run(
            r"git for-each-ref --format '%(objectname) %(refname:short)' refs/tags",
            hide=True,
        ).stdout.splitlines(),
    ):
        tags_per_commit.setdefault(commit, []).append(tag)

common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
    return PackageInfo(
        name=proj_metadata["name"],
        path=pkg_dir,
        language="py",
        version=proj_metadata["version"],
    )


def make_js_pkg_info(pkg_dir: Path) -> PackageInfo:
    with (pkg_dir / "package.json").open() as f:
        pkg_json = json.load(f)
    return PackageInfo(
        name=pkg_json["name"],
        path=pkg_dir,
        language="js",
        version=pkg_json["version"],
    )


@dataclass
class PackageInfo:
    name: str
    path: Path
    language: LanguageName
    version: str


def get_current_tags(context: Context) -> set[str]:
    """Get tags for the current commit"""
    # check if unstaged changes
    try:
        context.run("git diff --cached --exit-code", hide=True)
        context.run("git diff --exit-code", hide=True)
    except Exception:
        log.error("Cannot create a tag - there are uncommited changes")
        return set()

    tags_per_commit: dict[str, list[str]] = {}
    for commit, tag in map(
        str.split,
        context.run(
            r"git for-each-ref --format '%(objectname) %(refname:short)' refs/tags",
            hide=True,
        ).stdout.splitlines(),
    ):
        tags_per_commit.setdefault(commit, []).append(tag)

    current_commit = context.run(
        "git rev-parse HEAD", silent=True, external=True
    ).stdout.strip()
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
    return PackageInfo(
        name=proj_metadata["name"],
        path=pkg_dir,
        language="py",
        version=proj_metadata["version"],
    )


def make_js_pkg_info(pkg_dir: Path) -> PackageInfo:
    with (pkg_dir / "package.json").open() as f:
        pkg_json = json.load(f)
    return PackageInfo(
        name=pkg_json["name"],
        path=pkg_dir,
        language="js",
        version=pkg_json["version"],
    )


@dataclass
class PackageInfo:
    name: str
    path: Path
    language: LanguageName
    version: str


def get_current_tags(context: Context) -> set[str]:
    """Get tags for the current commit"""
    # check if unstaged changes
    try:
        context.run("git diff --cached --exit-code", hide=True)
        context.run("git diff --exit-code", hide=True)
    except Exception:
        log.error("Cannot create a tag - there are uncommited changes")
        return set()

    tags_per_commit: dict[str, list[str]] = {}
    for commit, tag in map(
        str.split,
        context.run(
            r"git for-each-ref --format '%(objectname) %(refname:short)' refs/tags",
            hide=True,
        ).stdout.splitlines(),
    ):
        tags_per_commit.setdefault(commit, []).append(tag)

    current_commit = context.run(
        "git rev-parse HEAD", silent=True, external=True
    ).stdout.strip()
    tags = set(tags_per_commit.get(current_commit, set()))

    if not tags:
        log.error("No tags found for current commit")

    for t in tags:
        if not TAG_PATTERN.match(t):
            msg = f"Invalid tag: {t}"
            raise Exit(msg)

    log.info(f"Found tags: {tags}")

    return tags


def parse_tag(tag: str) -> TagInfo:
    match = TAG_PATTERN.match(tag)
    if not match:
        msg = f"Invalid tag: {tag}"
        raise Exit(msg)

    version = match.group("version")
    if not semver.Version.is_valid(version):
        raise Exit(f"Invalid version: {version} in tag {tag}")

    return TagInfo(tag=tag, name=match.group("name"), version=match.group("version"))


@dataclass
class TagInfo:
    tag: str
    name: str
    version: str


def prepare_js_release(
    context: Context, package: PackageInfo
) -> Callable[[bool], None]:
    node_auth_token = os.getenv("NODE_AUTH_TOKEN")
    if node_auth_token is None:
        msg = "NODE_AUTH_TOKEN environment variable must be set"
        raise Exit(msg)

    with context.cd(JS_DIR):
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
    return PackageInfo(
        name=proj_metadata["name"],
        path=pkg_dir,
        language="py",
        version=proj_metadata["version"],
    )


def make_js_pkg_info(pkg_dir: Path) -> PackageInfo:
    with (pkg_dir / "package.json").open() as f:
        pkg_json = json.load(f)
    return PackageInfo(
        name=pkg_json["name"],
        path=pkg_dir,
        language="js",
        version=pkg_json["version"],
    )


@dataclass
class PackageInfo:
    name: str
    path: Path
    language: LanguageName
    version: str


def get_current_tags(context: Context) -> set[str]:
    """Get tags for the current commit"""
    # check if unstaged changes
    try:
        context.run("git diff --cached --exit-code", hide=True)
        context.run("git diff --exit-code", hide=True)
    except Exception:
        log.error("Cannot create a tag - there are uncommited changes")
        return set()

    tags_per_commit: dict[str, list[str]] = {}
    for commit, tag in map(
        str.split,
        context.run(
            r"git for-each-ref --format '%(objectname) %(refname:short)' refs/tags",
            hide=True,
        ).stdout.splitlines(),
    ):
        tags_per_commit.setdefault(commit, []).append(tag)

    current_commit = context.run(
        "git rev-parse HEAD", silent=True, external=True
    ).stdout.strip()
    tags = set(tags_per_commit.get(current_commit, set()))

    if not tags:
        log.error("No tags found for current commit")

    for t in tags:
        if not TAG_PATTERN.match(t):
            msg = f"Invalid tag: {t}"
            raise Exit(msg)

    log.info(f"Found tags: {tags}")

    return tags


def parse_tag(tag: str) -> TagInfo:
    match = TAG_PATTERN.match(tag)
    if not match:
        msg = f"Invalid tag: {tag}"
        raise Exit(msg)

    version = match.group("version")
    if not semver.Version.is_valid(version):
        raise Exit(f"Invalid version: {version} in tag {tag}")

    return TagInfo(tag=tag, name=match.group("name"), version=match.group("version"))


@dataclass
class TagInfo:
    tag: str
    name: str
    version: str


def prepare_js_release(
    context: Context, package: PackageInfo
) -> Callable[[bool], None]:
    node_auth_token = os.getenv("NODE_AUTH_TOKEN")
    if node_auth_token is None:
        msg = "NODE_AUTH_TOKEN environment variable must be set"
        raise Exit(msg)

    with context.cd(JS_DIR):
        context.run("npm ci")
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
    return PackageInfo(
        name=proj_metadata["name"],
        path=pkg_dir,
        language="py",
        version=proj_metadata["version"],
    )


def make_js_pkg_info(pkg_dir: Path) -> PackageInfo:
    with (pkg_dir / "package.json").open() as f:
        pkg_json = json.load(f)
    return PackageInfo(
        name=pkg_json["name"],
        path=pkg_dir,
        language="js",
        version=pkg_json["version"],
    )


@dataclass
class PackageInfo:
    name: str
    path: Path
    language: LanguageName
    version: str


def get_current_tags(context: Context) -> set[str]:
    """Get tags for the current commit"""
    # check if unstaged changes
    try:
        context.run("git diff --cached --exit-code", hide=True)
        context.run("git diff --exit-code", hide=True)
    except Exception:
        log.error("Cannot create a tag - there are uncommited changes")
        return set()

    tags_per_commit: dict[str, list[str]] = {}
    for commit, tag in map(
        str.split,
        context.run(
            r"git for-each-ref --format '%(objectname) %(refname:short)' refs/tags",
            hide=True,
        ).stdout.splitlines(),
    ):
        tags_per_commit.setdefault(commit, []).append(tag)

    current_commit = context.run(
        "git rev-parse HEAD", silent=True, external=True
    ).stdout.strip()
    tags = set(tags_per_commit.get(current_commit, set()))

    if not tags:
        log.error("No tags found for current commit")

    for t in tags:
        if not TAG_PATTERN.match(t):
            msg = f"Invalid tag: {t}"
            raise Exit(msg)

    log.info(f"Found tags: {tags}")

    return tags


def parse_tag(tag: str) -> TagInfo:
    match = TAG_PATTERN.match(tag)
    if not match:
        msg = f"Invalid tag: {tag}"
        raise Exit(msg)

    version = match.group("version")
    if not semver.Version.is_valid(version):
        raise Exit(f"Invalid version: {version} in tag {tag}")

    return TagInfo(tag=tag, name=match.group("name"), version=match.group("version"))


@dataclass
class TagInfo:
    tag: str
    name: str
    version: str


def prepare_js_release(
    context: Context, package: PackageInfo
) -> Callable[[bool], None]:
    node_auth_token = os.getenv("NODE_AUTH_TOKEN")
    if node_auth_token is None:
        msg = "NODE_AUTH_TOKEN environment variable must be set"
        raise Exit(msg)

    with context.cd(JS_DIR):
        context.run("npm ci")
        context.run("npm run build")

    def publish(dry_run: bool) -> None:
        with context.cd(JS_DIR):
            if dry_run:
common
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
    return PackageInfo(
        name=proj_metadata["name"],
        path=pkg_dir,
        language="py",
        version=proj_metadata["version"],
    )


def make_js_pkg_info(pkg_dir: Path) -> PackageInfo:
    with (pkg_dir / "package.json").open() as f:
        pkg_json = json.load(f)
    return PackageInfo(
        name=pkg_json["name"],
        path=pkg_dir,
        language="js",
        version=pkg_json["version"],
    )


@dataclass
class PackageInfo:
    name: str
    path: Path
    language: LanguageName
    version: str


non_informative
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
    return PackageInfo(
        name=proj_metadata["name"],
        path=pkg_dir,
        language="py",
        version=proj_metadata["version"],
    )


def make_js_pkg_info(pkg_dir: Path) -> PackageInfo:
    with (pkg_dir / "package.json").open() as f:
        pkg_json = json.load(f)
    return PackageInfo(
        name=pkg_json["name"],
        path=pkg_dir,
        language="js",
        version=pkg_json["version"],
    )


@dataclass
class PackageInfo:
    name: str
    path: Path
    language: LanguageName
    version: str


def get_current_tags(context: Context) -> set[str]:
    """Get tags for the current commit"""
    # check if unstaged changes
    try:
        context.run("git diff --cached --exit-code", hide=True)
        context.run("git diff --exit-code", hide=True)
    except Exception:
        log.error("Cannot create a tag - there are uncommited changes")
        return set()

    tags_per_commit: dict[str, list[str]] = {}
    for commit, tag in map(
        str.split,
        context.run(
            r"git for-each-ref --format '%(objectname) %(refname:short)' refs/tags",
            hide=True,
        ).stdout.splitlines(),
    ):
        tags_per_commit.setdefault(commit, []).append(tag)

    current_commit = context.run(
        "git rev-parse HEAD", silent=True, external=True
    ).stdout.strip()
    tags = set(tags_per_commit.get(current_commit, set()))

    if not tags:
        log.error("No tags found for current commit")

    for t in tags:
        if not TAG_PATTERN.match(t):
            msg = f"Invalid tag: {t}"
            raise Exit(msg)

    log.info(f"Found tags: {tags}")

    return tags


def parse_tag(tag: str) -> TagInfo:
    match = TAG_PATTERN.match(tag)
    if not match:
        msg = f"Invalid tag: {tag}"
        raise Exit(msg)

    version = match.group("version")
    if not semver.Version.is_valid(version):
        raise Exit(f"Invalid version: {version} in tag {tag}")

    return TagInfo(tag=tag, name=match.group("name"), version=match.group("version"))

non_informative
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


non_informative
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
    return PackageInfo(
        name=proj_metadata["name"],
        path=pkg_dir,
        language="py",
        version=proj_metadata["version"],
    )


def make_js_pkg_info(pkg_dir: Path) -> PackageInfo:
    with (pkg_dir / "package.json").open() as f:
        pkg_json = json.load(f)
    return PackageInfo(
        name=pkg_json["name"],
        path=pkg_dir,
        language="js",
        version=pkg_json["version"],
    )


@dataclass
class PackageInfo:
    name: str
    path: Path
    language: LanguageName
    version: str


def get_current_tags(context: Context) -> set[str]:
    """Get tags for the current commit"""
    # check if unstaged changes
    try:
        context.run("git diff --cached --exit-code", hide=True)
        context.run("git diff --exit-code", hide=True)
    except Exception:
random
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
    return PackageInfo(
        name=proj_metadata["name"],
        path=pkg_dir,
        language="py",
        version=proj_metadata["version"],
    )


def make_js_pkg_info(pkg_dir: Path) -> PackageInfo:
    with (pkg_dir / "package.json").open() as f:
        pkg_json = json.load(f)
    return PackageInfo(
        name=pkg_json["name"],
        path=pkg_dir,
        language="js",
        version=pkg_json["version"],
    )


@dataclass
class PackageInfo:
    name: str
    path: Path
    language: LanguageName
    version: str


def get_current_tags(context: Context) -> set[str]:
    """Get tags for the current commit"""
    # check if unstaged changes
    try:
        context.run("git diff --cached --exit-code", hide=True)
        context.run("git diff --exit-code", hide=True)
    except Exception:
        log.error("Cannot create a tag - there are uncommited changes")
        return set()

    tags_per_commit: dict[str, list[str]] = {}
    for commit, tag in map(
        str.split,
        context.run(
            r"git for-each-ref --format '%(objectname) %(refname:short)' refs/tags",
            hide=True,
        ).stdout.splitlines(),
    ):
        tags_per_commit.setdefault(commit, []).append(tag)

    current_commit = context.run(
        "git rev-parse HEAD", silent=True, external=True
    ).stdout.strip()
    tags = set(tags_per_commit.get(current_commit, set()))

    if not tags:
        log.error("No tags found for current commit")

    for t in tags:
        if not TAG_PATTERN.match(t):
            msg = f"Invalid tag: {t}"
            raise Exit(msg)

    log.info(f"Found tags: {tags}")

    return tags


def parse_tag(tag: str) -> TagInfo:
    match = TAG_PATTERN.match(tag)
    if not match:
        msg = f"Invalid tag: {tag}"
        raise Exit(msg)

    version = match.group("version")
    if not semver.Version.is_valid(version):
        raise Exit(f"Invalid version: {version} in tag {tag}")

    return TagInfo(tag=tag, name=match.group("name"), version=match.group("version"))


@dataclass
class TagInfo:
    tag: str
    name: str
    version: str


def prepare_js_release(
    context: Context, package: PackageInfo
) -> Callable[[bool], None]:
    node_auth_token = os.getenv("NODE_AUTH_TOKEN")
    if node_auth_token is None:
        msg = "NODE_AUTH_TOKEN environment variable must be set"
        raise Exit(msg)

    with context.cd(JS_DIR):
        context.run("npm ci")
        context.run("npm run build")

    def publish(dry_run: bool) -> None:
        with context.cd(JS_DIR):
            if dry_run:
                context.run(f"npm --workspace {package.name} pack --dry-run")
                return
            context.run(
                f"npm --workspace {package.name} publish --access public",
                env={"NODE_AUTH_TOKEN": node_auth_token},
            )

    return publish


def prepare_py_release(
    context: Context, package: PackageInfo
) -> Callable[[bool], None]:
    twine_username = os.getenv("PYPI_USERNAME")
    twine_password = os.getenv("PYPI_PASSWORD")

    if not (twine_password and twine_username):
        msg = "PYPI_USERNAME and PYPI_PASSWORD environment variables must be set"
        raise Exit(msg)

    for build_dir_name in ["build", "dist"]:
        build_dir_path = Path.cwd() / build_dir_name
        if build_dir_path.exists():
            rmtree(str(build_dir_path))

random
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
random
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
random
from __future__ import annotations

import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any, Callable

import semver
import toml
from invoke import task
from invoke.context import Context
from invoke.exceptions import Exit

# --- Typing Preamble ------------------------------------------------------------------


if TYPE_CHECKING:
    # not available in typing module until Python 3.8
    # not available in typing module until Python 3.10
    from typing import Literal, Protocol, TypeAlias

    class ReleasePrepFunc(Protocol):
        def __call__(
            self, context: Context, package: PackageInfo
        ) -> Callable[[bool], None]:
            ...

    LanguageName: TypeAlias = "Literal['py', 'js']"


# --- Constants ------------------------------------------------------------------------


log = logging.getLogger(__name__)
log.setLevel("INFO")
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(log_handler)


# --- Constants ------------------------------------------------------------------------


ROOT = Path(__file__).parent
DOCS_DIR = ROOT / "docs"
SRC_DIR = ROOT / "src"
JS_DIR = SRC_DIR / "js"
PY_DIR = SRC_DIR / "py"
PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]
TAG_PATTERN = re.compile(
    # start
    r"^"
    # package name
    r"(?P<name>[0-9a-zA-Z-@/]+)-"
    # package version
    r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
    # end
    r"$"
)


# --- Tasks ----------------------------------------------------------------------------


@task
def env(context: Context):
    """Install development environment"""
    env_py(context)
    env_js(context)


@task
def env_py(context: Context):
    """Install Python development environment"""
    for py_proj in PY_PROJECTS:
        py_proj_toml = toml.load(py_proj / "pyproject.toml")
        hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})
        hatch_default_features = hatch_default_env.get("features", [])
        hatch_default_deps = hatch_default_env.get("dependencies", [])
        with context.cd(py_proj):
            context.run(f"pip install '.[{','.join(hatch_default_features)}]'")
        context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")


@task
def env_js(context: Context):
    """Install JS development environment"""
    in_js(
        context,
        "npm ci",
        "npm run build",
        hide="out",
    )


@task
def lint_py(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        context.run("ruff --fix .")
    else:
        context.run("ruff .")
        context.run("black --check --diff .")
        in_py(
            context,
            f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",
            "hatch run lint:all",
        )


@task(pre=[env_js])
def lint_js(context: Context, fix: bool = False):
    """Run linters and type checkers"""
    if fix:
        in_js(context, "npm run fix:format")
    else:
        in_js(context, "npm run check:format")
    in_js(context, "npm run check:types")


@task
def test_py(context: Context, no_cov: bool = False):
    """Run test suites"""
    in_py(
        context,
        f"hatch run {'test' if no_cov else 'cov'} --maxfail=3 --reruns=3",
    )


@task(pre=[env_js])
def test_js(context: Context):
    """Run test suites"""
    in_js(context, "npm run check:tests")


@task(pre=[env_py])
def test_docs(context: Context):
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run sphinx-build "
            "-a "  # re-write all output files
            "-T "  # show full tracebacks
            "-W "  # turn warnings into errors
            "--keep-going "  # complete the build, but still report warnings as errors
            "-b doctest "
            "source "
            "build",
        )
        context.run("poetry run sphinx-build -b doctest source build")

    context.run("docker build . --file ./docs/Dockerfile")


@task
def docs(context: Context, docker: bool = False):
    """Build documentation"""
    if docker:
        _docker_docs(context)
    else:
        _live_docs(context)


def _docker_docs(context: Context) -> None:
    context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")
    context.run(
        "docker run -it -p 5000:5000 -e DEBUG=1 --rm reactpy-docs:latest", pty=True
    )


def _live_docs(context: Context) -> None:
    with context.cd(DOCS_DIR):
        context.run("poetry install")
        context.run(
            "poetry run python main.py "
            "--open-browser "
            # watch python source too
            "--watch=../src/py "
            # for some reason this matches absolute paths
            "--ignore=**/_auto/* "
            "--ignore=**/_static/custom.js "
            "--ignore=**/node_modules/* "
            "--ignore=**/package-lock.json "
            "-a "
            "-E "
            "-b "
            "html "
            "source "
            "build"
        )


@task
def publish(context: Context, dry_run: str = ""):
    """Publish packages that have been tagged for release in the current commit

    To perform a test run use `--dry-run=<name>-v<version>` to specify a comma-separated
    list of tags to simulate a release of. For example, to simulate a release of
    `@foo/bar-v1.2.3` and `baz-v4.5.6` use `--dry-run=@foo/bar-v1.2.3,baz-v4.5.6`.
    """
    packages = get_packages(context)

    release_prep: dict[LanguageName, ReleasePrepFunc] = {
        "js": prepare_js_release,
        "py": prepare_py_release,
    }

    parsed_tags: list[TagInfo] = [
        parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)
    ]

    publishers: list[Callable[[bool], None]] = []
    for tag_info in parsed_tags:
        if tag_info.name not in packages:
            msg = f"Tag {tag_info.tag} references package {tag_info.name} that does not exist"
            raise Exit(msg)

        pkg_info = packages[tag_info.name]
        if pkg_info.version != tag_info.version:
            msg = f"Tag {tag_info.tag} references version {tag_info.version} of package {tag_info.name}, but the current version is {pkg_info.version}"
            raise Exit(msg)

        log.info(f"Preparing {tag_info.name} for release...")
        publishers.append(release_prep[pkg_info.language](context, pkg_info))

    for publish in publishers:
        publish(bool(dry_run))


# --- Utilities ------------------------------------------------------------------------


def in_py(context: Context, *commands: str, **kwargs: Any) -> None:
    for p in PY_PROJECTS:
        with context.cd(p):
            log.info(f"Running commands in {p}...")
            for c in commands:
                context.run(c, **kwargs)


def in_js(context: Context, *commands: str, **kwargs: Any) -> None:
    with context.cd(JS_DIR):
        for c in commands:
            context.run(c, **kwargs)


def get_packages(context: Context) -> dict[str, PackageInfo]:
    packages: list[PackageInfo] = []

    for maybe_pkg in PY_DIR.glob("*"):
        if (maybe_pkg / "pyproject.toml").exists():
            packages.append(make_py_pkg_info(context, maybe_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_dir = JS_DIR / "packages"
    for maybe_pkg in packages_dir.glob("*"):
        if (maybe_pkg / "package.json").exists():
            packages.append(make_js_pkg_info(maybe_pkg))
        elif maybe_pkg.is_dir():
            for maybe_ns_pkg in maybe_pkg.glob("*"):
                if (maybe_ns_pkg / "package.json").exists():
                    packages.append(make_js_pkg_info(maybe_ns_pkg))
        else:
            msg = f"unexpected dir or file: {maybe_pkg}"
            raise Exit(msg)

    packages_by_name = {p.name: p for p in packages}
    if len(packages_by_name) != len(packages):
        raise Exit("duplicate package names detected")

    return packages_by_name


def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:
    with context.cd(pkg_dir):
        proj_metadata = json.loads(context.run("hatch project metadata").stdout)
    return PackageInfo(
        name=proj_metadata["name"],
        path=pkg_dir,
        language="py",
        version=proj_metadata["version"],
    )


def make_js_pkg_info(pkg_dir: Path) -> PackageInfo:
    with (pkg_dir / "package.json").open() as f:
        pkg_json = json.load(f)
    return PackageInfo(
        name=pkg_json["name"],
        path=pkg_dir,
        language="js",
        version=pkg_json["version"],
    )


@dataclass
class PackageInfo:
    name: str
    path: Path
    language: LanguageName
    version: str


def get_current_tags(context: Context) -> set[str]:
    """Get tags for the current commit"""
    # check if unstaged changes
    try:
        context.run("git diff --cached --exit-code", hide=True)
        context.run("git diff --exit-code", hide=True)
    except Exception:
        log.error("Cannot create a tag - there are uncommited changes")
        return set()

    tags_per_commit: dict[str, list[str]] = {}
    for commit, tag in map(
        str.split,
        context.run(
            r"git for-each-ref --format '%(objectname) %(refname:short)' refs/tags",
            hide=True,
random
        in_py(             packages.append(make_py_pkg_info(context, maybe_pkg))                 env={"NODE_AUTH_TOKEN": node_auth_token}, @task(pre=[env_js])     return publish             packages.append(make_js_pkg_info(maybe_pkg))     in_js(context, "npm run check:tests")     context: Context, package: PackageInfo @task(pre=[env_py])                     packages.append(make_js_pkg_info(maybe_ns_pkg))     return PackageInfo(             self, context: Context, package: PackageInfo         _docker_docs(context)         _live_docs(context)     env_py(context)     env_js(context)     packages = get_packages(context)     release_prep: dict[LanguageName, ReleasePrepFunc] = {         "js": prepare_js_release,         "py": prepare_py_release,     parsed_tags: list[TagInfo] = [         parse_tag(tag) for tag in dry_run.split(",") or get_current_tags(context)     in_js(     for publish in publishers:         publish(bool(dry_run))     return TagInfo(tag=tag, name=match.group("name"), version=match.group("version"))         in_js(context, "npm run fix:format")         in_js(context, "npm run check:format")     in_js(context, "npm run check:types")     packages: list[PackageInfo] = []             context.run(         context.run("poetry install")         context.run("hatch build")         proj_metadata = json.loads(context.run("hatch project metadata").stdout)         context.run("poetry run sphinx-build -b doctest source build")     context.run("docker build . --file ./docs/Dockerfile")                 context.run("twine check dist/*")         pkg_json = json.load(f)     context.run("docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest")         context.run("git diff --cached --exit-code", hide=True)         context.run("git diff --exit-code", hide=True)     current_commit = context.run(     tags = set(tags_per_commit.get(current_commit, set()))         py_proj_toml = toml.load(py_proj / "pyproject.toml")         hatch_default_env = py_proj_toml["tool"]["hatch"]["envs"].get("default", {})         hatch_default_features = hatch_default_env.get("features", [])         hatch_default_deps = hatch_default_env.get("dependencies", [])             context.run(f"pip install '.[{','.join(hatch_default_features)}]'")         context.run(f"pip install {' '.join(map(repr, hatch_default_deps))}")         context.run("ruff --fix .")         context.run("ruff .")         context.run("black --check --diff .")             context.run(c, **kwargs)         context.run("npm ci")         context.run("npm run build")                 context.run(f"npm --workspace {package.name} pack --dry-run") def get_current_tags(context: Context) -> set[str]: def make_py_pkg_info(context: Context, pkg_dir: Path) -> PackageInfo:         log.error("Cannot create a tag - there are uncommited changes")     with context.cd(package.path): PY_PROJECTS = [p for p in PY_DIR.iterdir() if (p / "pyproject.toml").exists()]             f"flake8 --toml-config {ROOT / 'pyproject.toml'} .",         ).stdout.splitlines(),