Repository: djc/askama
Pull request #613 (2022-01-25 14:19:50)

Remove support for deprecated `_parent` field
The support for the magic `_parent` field is deprecated since v0.8.0
or issue #180. It's bothersome to keep this feature alive, when no-one
should be using it for 3 years.

djc: Sounds good, but this should probably wait until we do another semver-incompatible release.
DamnWidget: You should also probably refactor all the test cases where `_parent` is being used 
michaelmelanson: What should we be using instead?
djc: @michaelmelanson what's your use case, and what have you tried that doesn't work?
Kijewski: Instead of removing the inheritance test entirely, I made it use ops::Deref.

Patch:
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 9102b42..5438379 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -33,7 +33,7 @@ jobs:
       matrix:
         package: [
           askama, askama_derive, testing,
-          askama_actix, askama_axum, askama_escape, askama_gotham,
+          askama_actix, askama_axum, askama_escape, askama_gotham, askama_hyper,
           askama_mendes, askama_rocket, askama_tide, askama_warp,
         ]
     runs-on: ubuntu-latest
diff --git a/Cargo.toml b/Cargo.toml
index 13f45d8..bd8b8da 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,6 +10,7 @@ members = [
     "askama_rocket",
     "askama_tide",
     "askama_warp",
+    "askama_hyper",
     "testing",
 ]
 
diff --git a/askama/Cargo.toml b/askama/Cargo.toml
index f421935..c6ca613 100644
--- a/askama/Cargo.toml
+++ b/askama/Cargo.toml
@@ -27,6 +27,7 @@ urlencode = ["askama_derive/urlencode", "percent-encoding"]
 with-actix-web = ["askama_derive/with-actix-web"]
 with-axum = ["askama_derive/with-axum"]
 with-gotham = ["askama_derive/with-gotham"]
+with-hyper = ["askama_derive/with-hyper"]
 with-mendes = ["askama_derive/with-mendes"]
 with-rocket = ["askama_derive/with-rocket"]
 with-tide = ["askama_derive/with-tide"]
diff --git a/askama_actix/Cargo.toml b/askama_actix/Cargo.toml
index ea95615..075f78e 100644
--- a/askama_actix/Cargo.toml
+++ b/askama_actix/Cargo.toml
@@ -18,7 +18,7 @@ askama = { version = "0.11.2", path = "../askama", default-features = false, fea
 
 [dev-dependencies]
 actix-rt = { version = "2", default-features = false }
-actix-test = "=0.1.0-beta.13"
+actix-test = "=0.1.0"
 bytes = { version = "1" }
 
 [features]
diff --git a/askama_derive/Cargo.toml b/askama_derive/Cargo.toml
index ce7abfa..1d176b7 100644
--- a/askama_derive/Cargo.toml
+++ b/askama_derive/Cargo.toml
@@ -23,6 +23,7 @@ num-traits = []
 with-actix-web = []
 with-axum = []
 with-gotham = []
+with-hyper = []
 with-mendes = []
 with-rocket = []
 with-tide = []
diff --git a/askama_derive/src/generator.rs b/askama_derive/src/generator.rs
index 142ff7f..5921609 100644
--- a/askama_derive/src/generator.rs
+++ b/askama_derive/src/generator.rs
@@ -293,11 +293,6 @@ impl<'a> Generator<'a> {
     // Takes a Context and generates the relevant implementations.
     fn build(mut self, ctx: &'a Context<'_>) -> Result<String, CompileError> {
         let mut buf = Buffer::new(0);
-        if !ctx.blocks.is_empty() {
-            if let Some(parent) = self.input.parent {
-                self.deref_to_parent(&mut buf, parent)?;
-            }
-        };
 
         self.impl_template(ctx, &mut buf)?;
         self.impl_display(&mut buf)?;
@@ -308,6 +303,8 @@ impl<'a> Generator<'a> {
         self.impl_axum_into_response(&mut buf)?;
         #[cfg(feature = "with-gotham")]
         self.impl_gotham_into_response(&mut buf)?;
+        #[cfg(feature = "with-hyper")]
+        self.impl_hyper_into_response(&mut buf)?;
         #[cfg(feature = "with-mendes")]
         self.impl_mendes_responder(&mut buf)?;
         #[cfg(feature = "with-rocket")]
@@ -376,24 +373,6 @@ impl<'a> Generator<'a> {
         Ok(())
     }
 
-    // Implement `Deref<Parent>` for an inheriting context struct.
-    fn deref_to_parent(
-        &mut self,
-        buf: &mut Buffer,
-        parent_type: &syn::Type,
-    ) -> Result<(), CompileError> {
-        self.write_header(buf, "::std::ops::Deref", None)?;
-        buf.writeln(&format!(
-            "type Target = {};",
-            parent_type.into_token_stream()
-        ))?;
-        buf.writeln("#[inline]")?;
-        buf.writeln("fn deref(&self) -> &Self::Target {")?;
-        buf.writeln("&self._parent")?;
-        buf.writeln("}")?;
-        buf.writeln("}")
-    }
-
     // Implement `Display` for the given context struct.
     fn impl_display(&mut self, buf: &mut Buffer) -> Result<(), CompileError> {
         self.write_header(buf, "::std::fmt::Display", None)?;
@@ -449,6 +428,31 @@ impl<'a> Generator<'a> {
         buf.writeln("}")
     }
 
+    // Implement `From<Template> for hyper::Response<Body>`.
+    #[cfg(feature = "with-hyper")]
+    fn impl_hyper_into_response(&mut self, buf: &mut Buffer) -> Result<(), CompileError> {
+        let (impl_generics, orig_ty_generics, where_clause) =
+            self.input.ast.generics.split_for_impl();
+        let ident = &self.input.ast.ident;
+        buf.writeln(&format!(
+            "{} {{",
+            quote!(
+                impl #impl_generics ::core::convert::From<&#ident #orig_ty_generics>
+                for ::askama_hyper::hyper::Response<::askama_hyper::hyper::Body>
+                #where_clause
+            )
+        ))?;
+        buf.writeln("#[inline]")?;
+        buf.writeln(&format!(
+            "{} {{",
+            quote!(fn from(value: &#ident #orig_ty_generics) -> Self)
+        ))?;
+        let ext = self.input.extension().unwrap_or("txt");
+        buf.writeln(&format!("::askama_hyper::respond(value, {:?})", ext))?;
+        buf.writeln("}")?;
+        buf.writeln("}")
+    }
+
     // Implement mendes' `Responder`.
     #[cfg(feature = "with-mendes")]
     fn impl_mendes_responder(&mut self, buf: &mut Buffer) -> Result<(), CompileError> {
diff --git a/askama_derive/src/input.rs b/askama_derive/src/input.rs
index c09f3d0..68d01db 100644
--- a/askama_derive/src/input.rs
+++ b/askama_derive/src/input.rs
@@ -16,15 +16,13 @@ pub(crate) struct TemplateInput<'a> {
     pub(crate) escaper: &'a str,
     pub(crate) ext: Option<String>,
     pub(crate) mime_type: String,
-    pub(crate) parent: Option<&'a syn::Type>,
     pub(crate) path: PathBuf,
 }
 
 impl TemplateInput<'_> {
     /// Extract the template metadata from the `DeriveInput` structure. This
     /// mostly recovers the data for the `TemplateInput` fields from the
-    /// `template()` attribute list fields; it also finds the of the `_parent`
-    /// field, if any.
+    /// `template()` attribute list fields.
     pub(crate) fn new<'n>(
         ast: &'n syn::DeriveInput,
         config: &'n Config<'_>,
@@ -51,27 +49,6 @@ impl TemplateInput<'_> {
             }
         };
 
-        // Check to see if a `_parent` field was defined on the context
-        // struct, and store the type for it for use in the code generator.
-        let parent = match ast.data {
-            syn::Data::Struct(syn::DataStruct {
-                fields: syn::Fields::Named(ref fields),
-                ..
-            }) => fields
-                .named
-                .iter()
-                .find(|f| f.ident.as_ref().filter(|name| *name == "_parent").is_some())
-                .map(|f| &f.ty),
-            _ => None,
-        };
-
-        if parent.is_some() {
-            eprint!(
-                "   --> in struct {}\n   = use of deprecated field '_parent'\n",
-                ast.ident
-            );
-        }
-
         // Validate syntax
         let syntax = syntax.map_or_else(
             || Ok(config.syntaxes.get(config.default_syntax).unwrap()),
@@ -117,7 +94,6 @@ impl TemplateInput<'_> {
             escaper,
             ext,
             mime_type,
-            parent,
             path,
         })
     }
diff --git a/askama_hyper/Cargo.toml b/askama_hyper/Cargo.toml
new file mode 100644
index 0000000..6a7a213
--- /dev/null
+++ b/askama_hyper/Cargo.toml
@@ -0,0 +1,33 @@
+[package]
+name = "askama_hyper"
+version = "0.1.0"
+description = "Hyper integration for Askama templates"
+documentation = "https://docs.rs/askama"
+keywords = ["markup", "template", "jinja2", "html"]
+categories = ["template-engine"]
+homepage = "https://github.com/djc/askama"
+repository = "https://github.com/djc/askama"
+license = "MIT OR Apache-2.0"
+workspace = ".."
+readme = "README.md"
+edition = "2018"
+
+[dependencies]
+askama = { version = "0.11.2", path = "../askama", default-features = false, features = ["with-hyper"] }
+hyper = { version = "0.14", default-features = false }
+
+[dev-dependencies]
+hyper = { version = "0.14", features = ["client"] }
+mime = "0.3"
+routerify = "3"
+tokio = { version = "1", default-features = false, features = ["net", "macros", "rt"] }
+
+[features]
+default = ["askama/default"]
+config = ["askama/config"]
+humansize = ["askama/humansize"]
+markdown = ["askama/markdown"]
+num-traits = ["askama/num-traits"]
+serde-json = ["askama/serde-json"]
+serde-yaml = ["askama/serde-yaml"]
+urlencode = ["askama/urlencode"]
diff --git a/askama_hyper/LICENSE-APACHE b/askama_hyper/LICENSE-APACHE
new file mode 120000
index 0000000..965b606
--- /dev/null
+++ b/askama_hyper/LICENSE-APACHE
@@ -0,0 +1,1 @@
+../LICENSE-APACHE
\ No newline at end of file
diff --git a/askama_hyper/LICENSE-MIT b/askama_hyper/LICENSE-MIT
new file mode 120000
index 0000000..76219eb
--- /dev/null
+++ b/askama_hyper/LICENSE-MIT
@@ -0,0 +1,1 @@
+../LICENSE-MIT
\ No newline at end of file
diff --git a/askama_hyper/README.md b/askama_hyper/README.md
new file mode 100644
index 0000000..024665a
--- /dev/null
+++ b/askama_hyper/README.md
@@ -0,0 +1,9 @@
+# askama_hyper: Askama integration with Hyper
+
+[![Documentation](https://docs.rs/askama_hyper/badge.svg)](https://docs.rs/askama_hyper/)
+[![Latest version](https://img.shields.io/crates/v/askama_hyper.svg)](https://crates.io/crates/askama_hyper)
+[![Build Status](https://github.com/djc/askama/workflows/CI/badge.svg)](https://github.com/djc/askama/actions?query=workflow%3ACI)
+[![Chat](https://badges.gitter.im/gitterHQ/gitter.svg)](https://gitter.im/djc/askama)
+
+Integration of the [Askama](https://github.com/djc/askama) templating engine in
+code building on the [hyper](https://crates.io/crates/hyper)-based web servers.
diff --git a/askama_hyper/src/lib.rs b/askama_hyper/src/lib.rs
new file mode 100644
index 0000000..07ede88
--- /dev/null
+++ b/askama_hyper/src/lib.rs
@@ -0,0 +1,29 @@
+#![forbid(unsafe_code)]
+#![deny(elided_lifetimes_in_paths)]
+#![deny(unreachable_pub)]
+
+pub use askama::*;
+#[doc(hidden)]
+pub use hyper;
+use hyper::{header, Body, Response, StatusCode};
+
+pub fn try_respond<T: Template>(t: &T, _ext: &str) -> Result<Response<Body>> {
+    Response::builder()
+        .status(StatusCode::OK)
+        .header(
+            header::CONTENT_TYPE,
+            header::HeaderValue::from_static(T::MIME_TYPE),
+        )
+        .body(t.render()?.into())
+        .map_err(|err| Error::Custom(Box::new(err)))
+}
+
+pub fn respond<T: Template>(t: &T, _ext: &str) -> Response<Body> {
+    match try_respond(t, _ext) {
+        Ok(response) => response,
+        Err(_) => Response::builder()
+            .status(StatusCode::INTERNAL_SERVER_ERROR)
+            .body(Body::empty())
+            .unwrap(),
+    }
+}
diff --git a/askama_hyper/templates/hello.html b/askama_hyper/templates/hello.html
new file mode 100644
index 0000000..8149be7
--- /dev/null
+++ b/askama_hyper/templates/hello.html
@@ -0,0 +1,1 @@
+Hello, {{ name }}!


Test patch:
diff --git a/askama_hyper/tests/basic.rs b/askama_hyper/tests/basic.rs
new file mode 100644
index 0000000..d0038c1
--- /dev/null
+++ b/askama_hyper/tests/basic.rs
@@ -0,0 +1,70 @@
+use std::convert::Infallible;
+
+use askama::Template;
+use hyper::body::to_bytes;
+use hyper::{Body, Client, Request, Response, Server};
+use routerify::ext::RequestExt;
+use routerify::{Router, RouterService};
+
+#[derive(Template)]
+#[template(path = "hello.html")]
+struct HelloTemplate<'a, N>
+where
+    N: std::fmt::Display,
+{
+    name: &'a N,
+}
+
+async fn hello_handler(req: Request<Body>) -> Result<Response<Body>, Infallible> {
+    let name = req.param("name").unwrap();
+    let template = &HelloTemplate { name: &name };
+    Ok(template.into())
+}
+
+fn router() -> Router<Body, Infallible> {
+    Router::builder()
+        .get("/hello/:name", hello_handler)
+        .build()
+        .unwrap()
+}
+
+#[tokio::test]
+async fn test_hyper() {
+    let addr = ([127, 0, 0, 1], 0).into();
+    let service = RouterService::new(router()).expect("Could not create service");
+    let server = Server::bind(&addr).serve(service);
+    let local_addr = server.local_addr();
+
+    let (tx, rx) = tokio::sync::oneshot::channel::<()>();
+    let serve = async move {
+        let server = server.with_graceful_shutdown(async {
+            rx.await.expect("Could not await signal to stop");
+        });
+        server.await.expect("Could not serve");
+    };
+    let query = async move {
+        let uri = format!("http://{}/hello/world", local_addr)
+            .parse()
+            .expect("Could not format URI");
+        let client = Client::new();
+
+        let res = client.get(uri).await.expect("Could not query client");
+        assert_eq!(res.status(), hyper::StatusCode::OK);
+
+        let content_type = res
+            .headers()
+            .get("content-type")
+            .expect("Response did not contain content-type header")
+            .to_str()
+            .expect("Content-type was not a UTF-8 string");
+        assert_eq!(content_type, mime::TEXT_HTML_UTF_8.to_string());
+
+        let body = to_bytes(res).await.expect("No body returned");
+        let body = std::str::from_utf8(&body).expect("Body was not UTF-8");
+        assert_eq!(body, "Hello, world!");
+
+        tx.send(()).unwrap();
+    };
+
+    tokio::join!(serve, query);
+}
diff --git a/testing/tests/inheritance.rs b/testing/tests/inheritance.rs
index 2dac362..5d8aefb 100644
--- a/testing/tests/inheritance.rs
+++ b/testing/tests/inheritance.rs
@@ -1,3 +1,5 @@
+use std::ops::Deref;
+
 use askama::Template;
 
 #[derive(Template)]
@@ -9,7 +11,15 @@ struct BaseTemplate<'a> {
 #[derive(Template)]
 #[template(path = "child.html")]
 struct ChildTemplate<'a> {
-    _parent: BaseTemplate<'a>,
+    _parent: &'a BaseTemplate<'a>,
+}
+
+impl<'a> Deref for ChildTemplate<'a> {
+    type Target = BaseTemplate<'a>;
+
+    fn deref(&self) -> &Self::Target {
+        self._parent
+    }
 }
 
 #[test]
@@ -21,7 +31,7 @@ fn test_use_base_directly() {
 #[test]
 fn test_simple_extends() {
     let t = ChildTemplate {
-        _parent: BaseTemplate { title: "Bar" },
+        _parent: &BaseTemplate { title: "Bar" },
     };
     assert_eq!(
         t.render().unwrap(),
@@ -43,6 +53,7 @@ fn test_empty_child() {
 
 pub mod parent {
     use askama::Template;
+
     #[derive(Template)]
     #[template(path = "base.html")]
     pub struct BaseTemplate<'a> {
@@ -53,17 +64,26 @@ pub mod parent {
 pub mod child {
     use super::parent::*;
     use askama::Template;
+
     #[derive(Template)]
     #[template(path = "child.html")]
     pub struct ChildTemplate<'a> {
-        pub _parent: BaseTemplate<'a>,
+        pub _parent: &'a BaseTemplate<'a>,
+    }
+
+    impl<'a> std::ops::Deref for ChildTemplate<'a> {
+        type Target = BaseTemplate<'a>;
+
+        fn deref(&self) -> &Self::Target {
+            self._parent
+        }
     }
 }
 
 #[test]
 fn test_different_module() {
     let t = child::ChildTemplate {
-        _parent: parent::BaseTemplate { title: "a" },
+        _parent: &parent::BaseTemplate { title: "a" },
     };
     assert_eq!(
         t.render().unwrap(),
@@ -81,6 +101,14 @@ struct NestedChildTemplate {
     _parent: NestedBaseTemplate,
 }
 
+impl Deref for NestedChildTemplate {
+    type Target = NestedBaseTemplate;
+
+    fn deref(&self) -> &Self::Target {
+        &self._parent
+    }
+}
+
 #[test]
 fn test_nested_blocks() {
     let t = NestedChildTemplate {
@@ -109,6 +137,14 @@ struct DeepKidTemplate {
     item: String,
 }
 
+impl Deref for DeepKidTemplate {
+    type Target = DeepMidTemplate;
+
+    fn deref(&self) -> &Self::Target {
+        &self._parent
+    }
+}
+
 #[test]
 fn test_deep() {
     let t = DeepKidTemplate {