Skip to content

App Basics

New Applications

Every view project will have a new_app call. The simplest app looks like this:

from view import new_app

app = new_app()

new_app does a few important things:

  • Loads the configuration, regardless of whether a config file exists.
  • Sets the App address for use by get_app (more on that later).
  • Loads finalization code for when the app closes.

While it's not required for every app, naming your app variable app is the proper convention for view, as that's the default variable searched for when using the view serve command, but more on that in a moment.

For now, just try to stick with naming your app file app.py and your view.App instance app.

view.app.App

Bases: ViewApp

Public view.py app object.

Source code in src/view/app.py
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
class App(ViewApp):
    """Public view.py app object."""

    def __init__(
        self,
        config: Config,
        *,
        error_class: type[Error] = Error,
    ) -> None:
        """
        Args:
            config: Configuration object to be used. Automatically generated by `new_app`.
        """
        supply_parsers(self)
        self.config = config
        self._set_dev_state(config.dev)
        self._manual_routes: list[Route] = []
        self.routes: list[Route] = []
        self.loaded: bool = False
        self.running = False
        self._docs: DocsType = {}
        self.loaded_routes: list[Route] = []
        self.templaters: dict[str, Any] = {}
        self._register_error(error_class)

        Service.log.setLevel(
            config.log.level
            if not isinstance(config.log.level, str)
            else config.log.level.upper()
        )

        if config.dev:
            if os.environ.get("VIEW_PROD") is not None:
                Service.warning("VIEW_PROD is set but dev is set to true")

            format_warnings()
            weakref.finalize(self, self._finalize)

            if config.log.pretty_tracebacks:
                install(show_locals=True)

            rich_handler = sys.excepthook

            def _hook(tp: type[B], value: B, traceback: Traceback) -> None:
                rich_handler(tp, value, traceback)
                os.environ["_VIEW_CANCEL_FINALIZERS"] = "1"

                if isinstance(value, ViewError):
                    if value.hint:
                        print(value.hint)

            sys.excepthook = _hook
            with suppress(UnsupportedOperation):
                faulthandler.enable()
        else:
            os.environ["VIEW_PROD"] = "1"

        if config.log.level == "debug":
            enable_debug()

        self.running = False

    def _finalize(self) -> None:
        if os.environ.get("_VIEW_CANCEL_FINALIZERS"):
            return

        if self.loaded:
            return

        warnings.warn(
            "load() was never called (did you forget to start the app?)"
        )
        split = self.config.app.app_path.split(":", maxsplit=1)

        if len(split) != 2:
            return

        app_name = split[1]

        print(
            make_hint(
                "Add this to your code",
                split[0],
                line=-1,
                prepend=f"\n{app_name}.run()",
            )
        )

    def _push_route(self, route: Route) -> None:
        if route in self._manual_routes:
            return

        self._manual_routes.append(route)

    def route(
        self,
        path_or_route: str | None | RouteOrCallable = None,
        doc: str | None = None,
        *,
        cache_rate: int = -1,
        methods: Iterable[StrMethod] | None = None,
    ) -> Callable[[RouteOrCallable], Route]:
        """Add a route that can be called with any method (or only specific methods).

        Args:
            path_or_route: The path to this route, or the route itself.
            doc: The description of the route to be used in documentation.
            cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.
            methods: Methods that can be used to access this route. If this is `None`, then all methods are allowed.

        Example:
            ```py
            from view import route

            @route("/", methods=("GET", "POST"))
            async def index():
                return "Hello, view.py!"
            ```
        """

        def inner(r: RouteOrCallable) -> Route:
            new_r = route_impl(
                path_or_route, doc, cache_rate=cache_rate, methods=methods
            )(r)
            self._push_route(new_r)
            return new_r

        return inner

    def _method_wrapper(
        self,
        path: str,
        doc: str | None,
        cache_rate: int,
        target: Callable[..., Any],
        # i dont really feel like typing this properly
    ) -> Callable[[RouteOrCallable], Route]:
        def inner(route: RouteOrCallable) -> Route:
            new_route = target(path, doc, cache_rate=cache_rate)(route)
            self._push_route(new_route)
            return new_route

        return inner

    def get(self, path: str, doc: str | None = None, *, cache_rate: int = -1):
        """Add a GET route.

        Args:
            path_or_route: The path to this route, or the route itself.
            doc: The description of the route to be used in documentation.
            cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.

        Example:
            ```py
            from view import new_app

            app = new_app()

            @app.get("/")
            async def index():
                return "Hello, view.py!"

            app.run()
            ```
        """
        return self._method_wrapper(path, doc, cache_rate, get)

    def post(self, path: str, doc: str | None = None, *, cache_rate: int = -1):
        """Add a POST route.

        Args:
            path_or_route: The path to this route, or the route itself.
            doc: The description of the route to be used in documentation.
            cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.

        Example:
            ```py
            from view import new_app

            app = new_app()

            @app.post("/")
            async def index():
                return "Hello, view.py!"

            app.run()
            ```
        """
        return self._method_wrapper(path, doc, cache_rate, post)

    def delete(
        self, path: str, doc: str | None = None, *, cache_rate: int = -1
    ):
        """Add a DELETE route.

        Args:
            path_or_route: The path to this route, or the route itself.
            doc: The description of the route to be used in documentation.
            cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.

        Example:
            ```py
            from view import new_app

            app = new_app()

            @app.delete("/")
            async def index():
                return "Hello, view.py!"

            app.run()
            ```
        """
        return self._method_wrapper(path, doc, cache_rate, delete)

    def patch(
        self,
        path: str,
        doc: str | None = None,
        *,
        cache_rate: int = -1,
    ):
        """Add a PATCH route.

        Args:
            path_or_route: The path to this route, or the route itself.
            doc: The description of the route to be used in documentation.
            cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.

        Example:
            ```py
            from view import new_app

            app = new_app()

            @app.patch("/")
            async def index():
                return "Hello, view.py!"

            app.run()
            ```
        """
        return self._method_wrapper(path, doc, cache_rate, patch)

    def put(self, path: str, doc: str | None = None, *, cache_rate: int = -1):
        """Add a PUT route.

        Args:
            path_or_route: The path to this route, or the route itself.
            doc: The description of the route to be used in documentation.
            cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.

        Example:
            ```py
            from view import new_app

            app = new_app()

            @app.put("/")
            async def index():
                return "Hello, view.py!"

            app.run()
            ```
        """
        return self._method_wrapper(path, doc, cache_rate, put)

    def options(
        self, path: str, doc: str | None = None, *, cache_rate: int = -1
    ):
        """Add an OPTIONS route.

        Args:
            path_or_route: The path to this route, or the route itself.
            doc: The description of the route to be used in documentation.
            cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.

        Example:
            ```py
            from view import new_app

            app = new_app()

            @app.options("/")
            async def index():
                return "Hello, view.py!"

            app.run()
            ```
        """
        return self._method_wrapper(path, doc, cache_rate, options)

    def _set_log_arg(self, kwargs: _LogArgs, key: str) -> None:
        if key not in kwargs:
            kwargs[key] = getattr(self.config.log.user, key)

    def _splat_log_args(self, kwargs: _LogArgs) -> _LogArgs:
        self._set_log_arg(kwargs, "log_file")
        self._set_log_arg(kwargs, "show_time")
        self._set_log_arg(kwargs, "show_caller")
        self._set_log_arg(kwargs, "show_color")
        self._set_log_arg(kwargs, "show_urgency")
        self._set_log_arg(kwargs, "file_write")
        self._set_log_arg(kwargs, "strftime")

        if "caller_frame" not in kwargs:
            frame = inspect.currentframe()
            assert frame, "failed to get frame"
            back = frame.f_back
            assert back, "frame has no f_back"
            kwargs["caller_frame"] = back

        return kwargs

    def debug(self, *messages: object, **kwargs: Unpack[_LogArgs]) -> None:
        log(*messages, urgency="debug", **self._splat_log_args(kwargs))

    def info(self, *messages: object, **kwargs: Unpack[_LogArgs]) -> None:
        log(*messages, urgency="info", **self._splat_log_args(kwargs))

    def warning(self, *messages: object, **kwargs: Unpack[_LogArgs]) -> None:
        log(*messages, urgency="warning", **self._splat_log_args(kwargs))

    def error(self, *messages: object, **kwargs: Unpack[_LogArgs]) -> None:
        log(*messages, urgency="error", **self._splat_log_args(kwargs))

    def critical(self, *messages: object, **kwargs: Unpack[_LogArgs]) -> None:
        log(*messages, urgency="critical", **self._splat_log_args(kwargs))

    def query(
        self,
        name: str,
        *tps: type[V],
        doc: str | None = None,
        default: V | None | _NoDefaultType = _NoDefault,
    ):
        """Set a query parameter.

        Args:
            name: Name of the parameter.
            tps: Types that can be passed to the server. If empty, any is used.
            doc: Description of this query parameter.
            default: Default value to be used if not supplied.
        """

        def inner(func: RouteOrCallable) -> Route:
            route = query_impl(name, *tps, doc=doc, default=default)(func)
            self._push_route(route)
            return route

        return inner

    def body(
        self,
        name: str,
        *tps: type[V],
        doc: str | None = None,
        default: V | None | _NoDefaultType = _NoDefault,
    ):
        """Set a body parameter.

        Args:
            name: Name of the parameter.
            tps: Types that can be passed to the server. If empty, any is used.
            doc: Description of this body parameter.
            default: Default value to be used if not supplied.
        """

        def inner(func: RouteOrCallable) -> Route:
            route = body_impl(name, *tps, doc=doc, default=default)(func)
            self._push_route(route)
            return route

        return inner

    async def template(
        self,
        name: str | Path,
        directory: str | Path | None = _ConfigSpecified,
        engine: TemplateEngine | None = _ConfigSpecified,
        frame: Frame | None | _CurrentFrameType = _CurrentFrame,
        **parameters: Any,
    ) -> HTML:
        """Render a template with the specified engine. This returns a view.py HTML response."""
        if frame is _CurrentFrame:
            f = inspect.currentframe()
            assert f
            f = f.f_back
            assert f
        else:
            f = frame

        return await template(
            name, directory, engine, f, app=self, **parameters
        )

    async def markdown(
        self,
        name: str | Path,
        *,
        directory: str | Path | None = _ConfigSpecified,
    ) -> HTML:
        """Convert a markdown file into HTML. This returns a view.py HTML response."""
        return await markdown(name, directory=directory, app=self)

    def context(self, r_or_none: RouteOrCallable | None = None):
        return context_impl(r_or_none)

    async def _app(self, scope, receive, send) -> None:
        return await self.asgi_app_entry(scope, receive, send)

    def load(self, routes: list[Route] | None = None) -> None:
        """Load the app. This is automatically called most of the time and should only be called manually during manual loading.

        Args:
            routes: Routes to load into the app.
        """
        if self.loaded:
            if routes:
                finalize(routes, self)
            Internal.warning("load called twice")
            return

        if routes and (self.config.app.loader != "manual"):
            warnings.warn(_ROUTES_WARN_MSG)

        if self.config.app.loader == "filesystem":
            load_fs(self, self.config.app.loader_path)
        elif self.config.app.loader == "simple":
            load_simple(self, self.config.app.loader_path)
        elif self.config.app.loader == "patterns":
            load_patterns(self, self.config.app.loader_path)
        else:
            finalize([*(routes or ()), *self._manual_routes], self)

        self.loaded = True

        from .routing import RouteInput

        for r in self.loaded_routes:
            if not r.path:
                continue

            body = {}
            query = {}

            for i in r.inputs:
                if not isinstance(i, RouteInput):
                    continue

                target = body if i.is_body else query
                target[i.name] = InputDoc(
                    i.doc or "No description provided.", i.tp, i.default
                )

            if r.method:
                self._docs[(r.method.name, r.path)] = RouteDoc(
                    r.doc or "No description provided.", body, query
                )
            else:
                self._docs[
                    (
                        tuple([i.name for i in r.method_list])
                        if r.method_list
                        else (
                            "GET",
                            "POST",
                            "PUT",
                            "PATCH",
                            "DELETE",
                            "OPTIONS",
                        ),
                        r.path,
                    )
                ] = RouteDoc(r.doc or "No description provided.", body, query)

    async def _spawn(self, coro: Coroutine[Any, Any, Any]):
        Internal.info(f"using event loop: {asyncio.get_event_loop()}")
        Internal.info(f"spawning {coro}")

        task = asyncio.create_task(coro)
        if self.config.log.hijack:
            if self.config.server.backend == "uvicorn":
                Internal.info("hijacking uvicorn")
                for log in (
                    logging.getLogger("uvicorn.error"),
                    logging.getLogger("uvicorn.access"),
                ):
                    log.addFilter(UvicornHijack())
            else:
                Internal.info("hijacking hypercorn")

        if self.config.log.fancy:
            if not self.config.log.hijack:
                raise ConfigurationError(
                    "hijack must be enabled for fancy mode"
                )

            enter_server()

        self.running = True
        Internal.debug("here we go!")
        await task
        self.running = False

        if self.config.log.fancy:
            exit_server()

        Internal.info("server closed")

    def _run(self, start_target: Callable[..., Any] | None = None) -> Any:
        self.load()
        Internal.info("starting server!")
        server = self.config.server.backend
        uvloop_enabled = False

        if self.config.app.uvloop is True:
            uvloop = importlib.import_module("uvloop")
            uvloop.install()
            uvloop_enabled = True
        elif self.config.app.uvloop == "decide":
            with suppress(ModuleNotFoundError):
                uvloop = importlib.import_module("uvloop")
                uvloop.install()
                uvloop_enabled = True

        start = start_target or asyncio.run

        if server == "uvicorn":
            config = uvicorn.Config(
                self._app,
                port=self.config.server.port,
                host=str(self.config.server.host),
                log_level="debug" if self.config.dev else "info",
                lifespan="on",
                factory=False,
                interface="asgi3",
                loop="uvloop" if uvloop_enabled else "asyncio",
                **self.config.server.extra_args,
            )
            server = uvicorn.Server(config)

            return start(self._spawn(server.serve()))

        elif server == "hypercorn":
            raise NotImplementedError
            conf = hypercorn.Config()
            conf.loglevel = "debug" if self.config.dev else "info"
            conf.bind = [
                f"{self.config.server.host}:{self.config.server.port}",
            ]

            for k, v in self.config.server.extra_args.items():
                setattr(conf, k, v)

            return start(
                importlib.import_module("hypercorn.asyncio").serve(
                    self._app, conf
                )
            )
        else:
            raise NotImplementedError("viewserver is not implemented yet")

    def run(self, *, fancy: bool | None = None) -> None:
        """Run the app."""
        if fancy is not None:
            self.config.log.fancy = fancy

        frame = inspect.currentframe()
        assert frame, "failed to get frame"
        assert frame.f_back, "frame has no f_back"

        back = frame.f_back
        base = os.path.basename(back.f_code.co_filename)
        app_path = self.config.app.app_path
        fname = app_path.split(":")[0]
        if base != fname:
            warnings.warn(
                f"ran app from {base}, but app path is {fname} in config",
            )

        if (not os.environ.get("_VIEW_RUN")) and (
            back.f_globals.get("__name__") == "__main__"
        ):
            self._run()
        else:
            Internal.info("called run, but env or scope prevented startup")

    def run_threaded(self, *, daemon: bool = True) -> Thread:
        """Run the app in a thread."""
        thread = Thread(target=self._run, daemon=daemon)
        thread.start()
        return thread

    def run_async(
        self,
        loop: asyncio.AbstractEventLoop | None = None,
    ) -> None:
        """Run the app in an event loop."""
        self._run((loop or asyncio.get_event_loop()).run_until_complete)

    def run_task(
        self,
        loop: asyncio.AbstractEventLoop | None = None,
    ) -> asyncio.Task[None]:
        """Run the app as a task."""
        return self._run((loop or asyncio.get_event_loop()).create_task)

    start = run

    def __repr__(self) -> str:
        return f"App(config={self.config!r})"

    @asynccontextmanager
    async def test(self):
        """Open the testing context."""
        self.load()
        ctx = TestingContext(self.asgi_app_entry)
        try:
            yield ctx
        finally:
            await ctx.stop()

    @overload
    def docs(self, file: None = None) -> str:
        ...

    @overload
    def docs(self, file: TextIO) -> None:
        ...

    @overload
    def docs(
        self,
        file: Path,
        *,
        encoding: str = "utf-8",
        overwrite: bool = True,
    ) -> None:
        ...

    @overload
    def docs(
        self,
        file: str,
        *,
        encoding: str = "utf-8",
        overwrite: bool = True,
    ) -> None:
        ...

    def docs(
        self,
        file: str | TextIO | Path | None = None,
        *,
        encoding: str = "utf-8",
        overwrite: bool = True,
    ) -> str | None:
        """Generate documentation for the app."""
        self.load()
        md = markdown_docs(self._docs)

        if not file:
            return md

        if isinstance(file, str):
            if not overwrite:
                Path(file).write_text(md, encoding=encoding)
            else:
                with open(file, "w", encoding=encoding) as f:
                    f.write(md)
        elif isinstance(file, Path):
            if overwrite:
                with open(file, "w", encoding=encoding) as f:
                    f.write(md)
            else:
                file.write_text(md)
        else:
            file.write(md)

view.app.App.__init__(config: Config, *, error_class: type[Error] = Error) -> None

Parameters:

Name Type Description Default
config Config

Configuration object to be used. Automatically generated by new_app.

required
Source code in src/view/app.py
def __init__(
    self,
    config: Config,
    *,
    error_class: type[Error] = Error,
) -> None:
    """
    Args:
        config: Configuration object to be used. Automatically generated by `new_app`.
    """
    supply_parsers(self)
    self.config = config
    self._set_dev_state(config.dev)
    self._manual_routes: list[Route] = []
    self.routes: list[Route] = []
    self.loaded: bool = False
    self.running = False
    self._docs: DocsType = {}
    self.loaded_routes: list[Route] = []
    self.templaters: dict[str, Any] = {}
    self._register_error(error_class)

    Service.log.setLevel(
        config.log.level
        if not isinstance(config.log.level, str)
        else config.log.level.upper()
    )

    if config.dev:
        if os.environ.get("VIEW_PROD") is not None:
            Service.warning("VIEW_PROD is set but dev is set to true")

        format_warnings()
        weakref.finalize(self, self._finalize)

        if config.log.pretty_tracebacks:
            install(show_locals=True)

        rich_handler = sys.excepthook

        def _hook(tp: type[B], value: B, traceback: Traceback) -> None:
            rich_handler(tp, value, traceback)
            os.environ["_VIEW_CANCEL_FINALIZERS"] = "1"

            if isinstance(value, ViewError):
                if value.hint:
                    print(value.hint)

        sys.excepthook = _hook
        with suppress(UnsupportedOperation):
            faulthandler.enable()
    else:
        os.environ["VIEW_PROD"] = "1"

    if config.log.level == "debug":
        enable_debug()

    self.running = False

view.app.App.body(name: str, *tps: type[V], doc: str | None = None, default: V | None | _NoDefaultType = _NoDefault)

Set a body parameter.

Parameters:

Name Type Description Default
name str

Name of the parameter.

required
tps type[V]

Types that can be passed to the server. If empty, any is used.

()
doc str | None

Description of this body parameter.

None
default V | None | _NoDefaultType

Default value to be used if not supplied.

_NoDefault
Source code in src/view/app.py
def body(
    self,
    name: str,
    *tps: type[V],
    doc: str | None = None,
    default: V | None | _NoDefaultType = _NoDefault,
):
    """Set a body parameter.

    Args:
        name: Name of the parameter.
        tps: Types that can be passed to the server. If empty, any is used.
        doc: Description of this body parameter.
        default: Default value to be used if not supplied.
    """

    def inner(func: RouteOrCallable) -> Route:
        route = body_impl(name, *tps, doc=doc, default=default)(func)
        self._push_route(route)
        return route

    return inner

view.app.App.delete(path: str, doc: str | None = None, *, cache_rate: int = -1)

Add a DELETE route.

Parameters:

Name Type Description Default
path_or_route

The path to this route, or the route itself.

required
doc str | None

The description of the route to be used in documentation.

None
cache_rate int

Reload the cache for this route every x number of requests. -1 means to never cache.

-1
Example
from view import new_app

app = new_app()

@app.delete("/")
async def index():
    return "Hello, view.py!"

app.run()
Source code in src/view/app.py
def delete(
    self, path: str, doc: str | None = None, *, cache_rate: int = -1
):
    """Add a DELETE route.

    Args:
        path_or_route: The path to this route, or the route itself.
        doc: The description of the route to be used in documentation.
        cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.

    Example:
        ```py
        from view import new_app

        app = new_app()

        @app.delete("/")
        async def index():
            return "Hello, view.py!"

        app.run()
        ```
    """
    return self._method_wrapper(path, doc, cache_rate, delete)

view.app.App.docs(file: str | TextIO | Path | None = None, *, encoding: str = 'utf-8', overwrite: bool = True) -> str | None

Generate documentation for the app.

Source code in src/view/app.py
def docs(
    self,
    file: str | TextIO | Path | None = None,
    *,
    encoding: str = "utf-8",
    overwrite: bool = True,
) -> str | None:
    """Generate documentation for the app."""
    self.load()
    md = markdown_docs(self._docs)

    if not file:
        return md

    if isinstance(file, str):
        if not overwrite:
            Path(file).write_text(md, encoding=encoding)
        else:
            with open(file, "w", encoding=encoding) as f:
                f.write(md)
    elif isinstance(file, Path):
        if overwrite:
            with open(file, "w", encoding=encoding) as f:
                f.write(md)
        else:
            file.write_text(md)
    else:
        file.write(md)

view.app.App.get(path: str, doc: str | None = None, *, cache_rate: int = -1)

Add a GET route.

Parameters:

Name Type Description Default
path_or_route

The path to this route, or the route itself.

required
doc str | None

The description of the route to be used in documentation.

None
cache_rate int

Reload the cache for this route every x number of requests. -1 means to never cache.

-1
Example
from view import new_app

app = new_app()

@app.get("/")
async def index():
    return "Hello, view.py!"

app.run()
Source code in src/view/app.py
def get(self, path: str, doc: str | None = None, *, cache_rate: int = -1):
    """Add a GET route.

    Args:
        path_or_route: The path to this route, or the route itself.
        doc: The description of the route to be used in documentation.
        cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.

    Example:
        ```py
        from view import new_app

        app = new_app()

        @app.get("/")
        async def index():
            return "Hello, view.py!"

        app.run()
        ```
    """
    return self._method_wrapper(path, doc, cache_rate, get)

view.app.App.load(routes: list[Route] | None = None) -> None

Load the app. This is automatically called most of the time and should only be called manually during manual loading.

Parameters:

Name Type Description Default
routes list[Route] | None

Routes to load into the app.

None
Source code in src/view/app.py
def load(self, routes: list[Route] | None = None) -> None:
    """Load the app. This is automatically called most of the time and should only be called manually during manual loading.

    Args:
        routes: Routes to load into the app.
    """
    if self.loaded:
        if routes:
            finalize(routes, self)
        Internal.warning("load called twice")
        return

    if routes and (self.config.app.loader != "manual"):
        warnings.warn(_ROUTES_WARN_MSG)

    if self.config.app.loader == "filesystem":
        load_fs(self, self.config.app.loader_path)
    elif self.config.app.loader == "simple":
        load_simple(self, self.config.app.loader_path)
    elif self.config.app.loader == "patterns":
        load_patterns(self, self.config.app.loader_path)
    else:
        finalize([*(routes or ()), *self._manual_routes], self)

    self.loaded = True

    from .routing import RouteInput

    for r in self.loaded_routes:
        if not r.path:
            continue

        body = {}
        query = {}

        for i in r.inputs:
            if not isinstance(i, RouteInput):
                continue

            target = body if i.is_body else query
            target[i.name] = InputDoc(
                i.doc or "No description provided.", i.tp, i.default
            )

        if r.method:
            self._docs[(r.method.name, r.path)] = RouteDoc(
                r.doc or "No description provided.", body, query
            )
        else:
            self._docs[
                (
                    tuple([i.name for i in r.method_list])
                    if r.method_list
                    else (
                        "GET",
                        "POST",
                        "PUT",
                        "PATCH",
                        "DELETE",
                        "OPTIONS",
                    ),
                    r.path,
                )
            ] = RouteDoc(r.doc or "No description provided.", body, query)

view.app.App.markdown(name: str | Path, *, directory: str | Path | None = _ConfigSpecified) -> HTML async

Convert a markdown file into HTML. This returns a view.py HTML response.

Source code in src/view/app.py
async def markdown(
    self,
    name: str | Path,
    *,
    directory: str | Path | None = _ConfigSpecified,
) -> HTML:
    """Convert a markdown file into HTML. This returns a view.py HTML response."""
    return await markdown(name, directory=directory, app=self)

view.app.App.options(path: str, doc: str | None = None, *, cache_rate: int = -1)

Add an OPTIONS route.

Parameters:

Name Type Description Default
path_or_route

The path to this route, or the route itself.

required
doc str | None

The description of the route to be used in documentation.

None
cache_rate int

Reload the cache for this route every x number of requests. -1 means to never cache.

-1
Example
from view import new_app

app = new_app()

@app.options("/")
async def index():
    return "Hello, view.py!"

app.run()
Source code in src/view/app.py
def options(
    self, path: str, doc: str | None = None, *, cache_rate: int = -1
):
    """Add an OPTIONS route.

    Args:
        path_or_route: The path to this route, or the route itself.
        doc: The description of the route to be used in documentation.
        cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.

    Example:
        ```py
        from view import new_app

        app = new_app()

        @app.options("/")
        async def index():
            return "Hello, view.py!"

        app.run()
        ```
    """
    return self._method_wrapper(path, doc, cache_rate, options)

view.app.App.patch(path: str, doc: str | None = None, *, cache_rate: int = -1)

Add a PATCH route.

Parameters:

Name Type Description Default
path_or_route

The path to this route, or the route itself.

required
doc str | None

The description of the route to be used in documentation.

None
cache_rate int

Reload the cache for this route every x number of requests. -1 means to never cache.

-1
Example
from view import new_app

app = new_app()

@app.patch("/")
async def index():
    return "Hello, view.py!"

app.run()
Source code in src/view/app.py
def patch(
    self,
    path: str,
    doc: str | None = None,
    *,
    cache_rate: int = -1,
):
    """Add a PATCH route.

    Args:
        path_or_route: The path to this route, or the route itself.
        doc: The description of the route to be used in documentation.
        cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.

    Example:
        ```py
        from view import new_app

        app = new_app()

        @app.patch("/")
        async def index():
            return "Hello, view.py!"

        app.run()
        ```
    """
    return self._method_wrapper(path, doc, cache_rate, patch)

view.app.App.post(path: str, doc: str | None = None, *, cache_rate: int = -1)

Add a POST route.

Parameters:

Name Type Description Default
path_or_route

The path to this route, or the route itself.

required
doc str | None

The description of the route to be used in documentation.

None
cache_rate int

Reload the cache for this route every x number of requests. -1 means to never cache.

-1
Example
from view import new_app

app = new_app()

@app.post("/")
async def index():
    return "Hello, view.py!"

app.run()
Source code in src/view/app.py
def post(self, path: str, doc: str | None = None, *, cache_rate: int = -1):
    """Add a POST route.

    Args:
        path_or_route: The path to this route, or the route itself.
        doc: The description of the route to be used in documentation.
        cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.

    Example:
        ```py
        from view import new_app

        app = new_app()

        @app.post("/")
        async def index():
            return "Hello, view.py!"

        app.run()
        ```
    """
    return self._method_wrapper(path, doc, cache_rate, post)

view.app.App.put(path: str, doc: str | None = None, *, cache_rate: int = -1)

Add a PUT route.

Parameters:

Name Type Description Default
path_or_route

The path to this route, or the route itself.

required
doc str | None

The description of the route to be used in documentation.

None
cache_rate int

Reload the cache for this route every x number of requests. -1 means to never cache.

-1
Example
from view import new_app

app = new_app()

@app.put("/")
async def index():
    return "Hello, view.py!"

app.run()
Source code in src/view/app.py
def put(self, path: str, doc: str | None = None, *, cache_rate: int = -1):
    """Add a PUT route.

    Args:
        path_or_route: The path to this route, or the route itself.
        doc: The description of the route to be used in documentation.
        cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.

    Example:
        ```py
        from view import new_app

        app = new_app()

        @app.put("/")
        async def index():
            return "Hello, view.py!"

        app.run()
        ```
    """
    return self._method_wrapper(path, doc, cache_rate, put)

view.app.App.query(name: str, *tps: type[V], doc: str | None = None, default: V | None | _NoDefaultType = _NoDefault)

Set a query parameter.

Parameters:

Name Type Description Default
name str

Name of the parameter.

required
tps type[V]

Types that can be passed to the server. If empty, any is used.

()
doc str | None

Description of this query parameter.

None
default V | None | _NoDefaultType

Default value to be used if not supplied.

_NoDefault
Source code in src/view/app.py
def query(
    self,
    name: str,
    *tps: type[V],
    doc: str | None = None,
    default: V | None | _NoDefaultType = _NoDefault,
):
    """Set a query parameter.

    Args:
        name: Name of the parameter.
        tps: Types that can be passed to the server. If empty, any is used.
        doc: Description of this query parameter.
        default: Default value to be used if not supplied.
    """

    def inner(func: RouteOrCallable) -> Route:
        route = query_impl(name, *tps, doc=doc, default=default)(func)
        self._push_route(route)
        return route

    return inner

view.app.App.route(path_or_route: str | None | RouteOrCallable = None, doc: str | None = None, *, cache_rate: int = -1, methods: Iterable[StrMethod] | None = None) -> Callable[[RouteOrCallable], Route]

Add a route that can be called with any method (or only specific methods).

Parameters:

Name Type Description Default
path_or_route str | None | RouteOrCallable

The path to this route, or the route itself.

None
doc str | None

The description of the route to be used in documentation.

None
cache_rate int

Reload the cache for this route every x number of requests. -1 means to never cache.

-1
methods Iterable[StrMethod] | None

Methods that can be used to access this route. If this is None, then all methods are allowed.

None
Example
from view import route

@route("/", methods=("GET", "POST"))
async def index():
    return "Hello, view.py!"
Source code in src/view/app.py
def route(
    self,
    path_or_route: str | None | RouteOrCallable = None,
    doc: str | None = None,
    *,
    cache_rate: int = -1,
    methods: Iterable[StrMethod] | None = None,
) -> Callable[[RouteOrCallable], Route]:
    """Add a route that can be called with any method (or only specific methods).

    Args:
        path_or_route: The path to this route, or the route itself.
        doc: The description of the route to be used in documentation.
        cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.
        methods: Methods that can be used to access this route. If this is `None`, then all methods are allowed.

    Example:
        ```py
        from view import route

        @route("/", methods=("GET", "POST"))
        async def index():
            return "Hello, view.py!"
        ```
    """

    def inner(r: RouteOrCallable) -> Route:
        new_r = route_impl(
            path_or_route, doc, cache_rate=cache_rate, methods=methods
        )(r)
        self._push_route(new_r)
        return new_r

    return inner

view.app.App.run(*, fancy: bool | None = None) -> None

Run the app.

Source code in src/view/app.py
def run(self, *, fancy: bool | None = None) -> None:
    """Run the app."""
    if fancy is not None:
        self.config.log.fancy = fancy

    frame = inspect.currentframe()
    assert frame, "failed to get frame"
    assert frame.f_back, "frame has no f_back"

    back = frame.f_back
    base = os.path.basename(back.f_code.co_filename)
    app_path = self.config.app.app_path
    fname = app_path.split(":")[0]
    if base != fname:
        warnings.warn(
            f"ran app from {base}, but app path is {fname} in config",
        )

    if (not os.environ.get("_VIEW_RUN")) and (
        back.f_globals.get("__name__") == "__main__"
    ):
        self._run()
    else:
        Internal.info("called run, but env or scope prevented startup")

view.app.App.run_async(loop: asyncio.AbstractEventLoop | None = None) -> None

Run the app in an event loop.

Source code in src/view/app.py
def run_async(
    self,
    loop: asyncio.AbstractEventLoop | None = None,
) -> None:
    """Run the app in an event loop."""
    self._run((loop or asyncio.get_event_loop()).run_until_complete)

view.app.App.run_task(loop: asyncio.AbstractEventLoop | None = None) -> asyncio.Task[None]

Run the app as a task.

Source code in src/view/app.py
def run_task(
    self,
    loop: asyncio.AbstractEventLoop | None = None,
) -> asyncio.Task[None]:
    """Run the app as a task."""
    return self._run((loop or asyncio.get_event_loop()).create_task)

view.app.App.run_threaded(*, daemon: bool = True) -> Thread

Run the app in a thread.

Source code in src/view/app.py
def run_threaded(self, *, daemon: bool = True) -> Thread:
    """Run the app in a thread."""
    thread = Thread(target=self._run, daemon=daemon)
    thread.start()
    return thread

view.app.App.template(name: str | Path, directory: str | Path | None = _ConfigSpecified, engine: TemplateEngine | None = _ConfigSpecified, frame: Frame | None | _CurrentFrameType = _CurrentFrame, **parameters: Any) -> HTML async

Render a template with the specified engine. This returns a view.py HTML response.

Source code in src/view/app.py
async def template(
    self,
    name: str | Path,
    directory: str | Path | None = _ConfigSpecified,
    engine: TemplateEngine | None = _ConfigSpecified,
    frame: Frame | None | _CurrentFrameType = _CurrentFrame,
    **parameters: Any,
) -> HTML:
    """Render a template with the specified engine. This returns a view.py HTML response."""
    if frame is _CurrentFrame:
        f = inspect.currentframe()
        assert f
        f = f.f_back
        assert f
    else:
        f = frame

    return await template(
        name, directory, engine, f, app=self, **parameters
    )

view.app.App.test() async

Open the testing context.

Source code in src/view/app.py
@asynccontextmanager
async def test(self):
    """Open the testing context."""
    self.load()
    ctx = TestingContext(self.asgi_app_entry)
    try:
        yield ctx
    finally:
        await ctx.stop()

Launching Apps

Python libraries generally have two ways to run a web server:

  • Running via the command line.
  • Launching from Python itself (e.g. a server.start(...) function).

Both have their benefits and downsides, so view.py supports both out of the box. App comes with its run() method, and the view CLI has the view serve command.

Generally, you're going to want to add an app.run() to every view.py project, like so:

from view import new_app

app = new_app()
app.run()

This way, if you (or someone else) want to run your code programmatically, they can run it via something like python3 app.py. It's also more semantically clear that an app is going to start when you run that file.

If you prefer the CLI method, you can just run view serve and view.py will extract the app from the file itself, ignoring the run() call.

Note that this behavior is a double-edged sword, so be careful. When calling with run(), the Python script will never get past that line because the server will run indefinitely, but when using view serve it proceeds past it just fine since all it's doing is extracting the app, skipping run(). For example, take a look at this code:

from view import new_app

app = new_app()
app.run()
print("you called the app with view serve")  # this only runs when `view serve` is used

view.app.App.run(*, fancy: bool | None = None) -> None

Run the app.

Source code in src/view/app.py
def run(self, *, fancy: bool | None = None) -> None:
    """Run the app."""
    if fancy is not None:
        self.config.log.fancy = fancy

    frame = inspect.currentframe()
    assert frame, "failed to get frame"
    assert frame.f_back, "frame has no f_back"

    back = frame.f_back
    base = os.path.basename(back.f_code.co_filename)
    app_path = self.config.app.app_path
    fname = app_path.split(":")[0]
    if base != fname:
        warnings.warn(
            f"ran app from {base}, but app path is {fname} in config",
        )

    if (not os.environ.get("_VIEW_RUN")) and (
        back.f_globals.get("__name__") == "__main__"
    ):
        self._run()
    else:
        Internal.info("called run, but env or scope prevented startup")

Fancy Mode

View comes with something called "fancy mode", which is a fancy UI that shows when you run the app. If you would like to disable this, you can do one of two things:

  • Disable the fancy setting in configuration.
  • Pass fancy=False to run().

You should disable it in the configuration if you completely despise fancy mode and don't want to use it at all, but if you only want to temporarily turn it off (for example, if you're a view.py developer and need to see proper output) then pass fancy=False.

Getting the App

Circular Imports

If you've worked with big Python projects before, there's a good chance you've run into a circular import error. A circular import error occurs when two modules try to import each other. A view.py example of this problem would most likely be the main app file trying to import a route, but then that route tries to import the app.

Note

The below example uses routing, which if you're reading this for the first time you don't know how to use yet. Focus on the use of the app variable and not the routing itself.

# app.py
from view import new_app
from routes import my_route

app = new_app()
app.load([my_route])
app.run()
# routes.py
from view import get
from app import app

@app.get("/something")
def something():
    return "something"

@get("/")
def index():
    return "Hello, view.py"

View gives you a solution to this problem: get_app. get_app uses some magic internally to get you your App instance right then and there, no import required. It works similar to how you would use new_app:

from view import get_app

app = get_app()

@app.get("/")
def index():
    return "..."

view.app.get_app(*, address: int | None = None) -> App

Get the last app created by new_app.

Source code in src/view/app.py
def get_app(*, address: int | None = None) -> App:
    """Get the last app created by `new_app`."""
    env = os.environ.get("_VIEW_APP_ADDRESS")
    addr = address or env

    if (not addr) and (not env):
        raise BadEnvironmentError("no view app registered")

    app: App = ctypes.cast(int(addr), ctypes.py_object).value  # type: ignore
    ctypes.pythonapi.Py_IncRef(app)
    return app

Review

Every view.py project should contain a call to new_app. new_app does important things like loading your configuration, set's up finalization code, and letting the App instance be used by get_app.

Running an app can be done in two ways: programmatically via the App.run or through view serve command. However, every view.py app should contain an App.run to give the choice for running programmatically. By default, view.py has a fancy UI when running your app, which may be disabled via editing the config or passing fancy=False to run().

Finally, circular imports occur when two Python modules try to import each other, which can happen a lot in view when getting the app from the app file (especially in manual routing). To fix it, View provides a get_app function to get you your App instance without an import.